Il 0% ha trovato utile questo documento (0 voti)
36 visualizzazioni401 pagine

The Go Programming Language It

Caricato da

gr.benincasa
Copyright
© © All Rights Reserved
Per noi i diritti sui contenuti sono una cosa seria. Se sospetti che questo contenuto sia tuo, rivendicalo qui.
Formati disponibili
Scarica in formato PDF, TXT o leggi online su Scribd
Il 0% ha trovato utile questo documento (0 voti)
36 visualizzazioni401 pagine

The Go Programming Language It

Caricato da

gr.benincasa
Copyright
© © All Rights Reserved
Per noi i diritti sui contenuti sono una cosa seria. Se sospetti che questo contenuto sia tuo, rivendicalo qui.
Formati disponibili
Scarica in formato PDF, TXT o leggi online su Scribd
Sei sulla pagina 1/ 401

www.it-ebooks.

info
Il linguaggio di
programmazione Go

www.it-ebooks.info
Questa pagina è stata lasciata intenzionalmente in bianco

www.it-ebooks.info
Il linguaggio di
programmazione Go

Alan A. A. Donovan
Google Inc.

Brian W. Kernighan
Università di Princeton

New York - Boston - Indianapolis - San Francisco Toronto -


Montreal - Londra - Monaco - Parigi - Madrid Capetown -
Sydney - Tokyo - Singapore - Città del Messico

www.it-ebooks.info
Molte delle denominazioni utilizzate da produttori e venditori per distinguere i loro prodotti sono rivendicate
come marchi di fabbrica. Nei casi in cui tali denominazioni appaiono in questo libro e l'editore era a conoscenza
di una rivendicazione di marchio, le denominazioni sono state stampate con le iniziali maiuscole o in tutte
maiuscole.
Gli autori e l'editore hanno curato la preparazione di questo libro, ma non forniscono alcuna garanzia espressa o
implicita di alcun tipo e non si assumono alcuna responsabilità per errori od omissioni. Non si assume alcuna
responsabilità per danni incidentali o consequenziali in relazione o derivanti dall'uso delle informazioni o dei
programmi qui contenuti.
Per informazioni sull'acquisto di questo titolo in quantità massicce o per opportunità di vendita speciali (che
possono includere versioni elettroniche, copertine personalizzate e contenuti specifici per la vostra azienda, i
vostri obiettivi di formazione, il vostro marketing o i vostri interessi di branding), contattate il nostro reparto
vendite aziendali all'indirizzo [email protected] o (800) 382-3419.
Per richieste di informazioni sulle vendite governative, contattare [email protected].
Per domande sulle vendite al di fuori degli Stati Uniti, contattare [email protected]. Visitateci sul
Web: informit.com/aw
Numero di controllo della Biblioteca del Congresso: 2015950709
Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan
Tutti i diritti riservati. Stampato negli Stati Uniti d'America. Questa pubblicazione è protetta dal diritto d'autore e
l'autorizzazione deve essere ottenuta dall'editore prima di qualsiasi riproduzione, memorizzazione in un sistema
di recupero o trasmissione vietata in qualsiasi forma o con qualsiasi mezzo, elettronico, meccanico, di
fotocopiatura, registrazione o altro. Per ottenere l'autorizzazione all'uso di materiale tratto da quest'opera, si
prega di inviare una richiesta scritta a Pearson Education, Inc., Permissions Department, 200 Old Tappan Road, Old
Tappan, New Jersey 07675, oppure si può inviare la richiesta via fax al numero (201) 236-3290.
Prima di copertina: Viadotto di Millau, valle del Tarn, Francia meridionale. Esempio di semplicità nel design
dell'ingegneria moderna, il viadotto ha sostituito un percorso contorto dalla capitale alla costa con un percorso
diretto sopra le nuvole. © Jean-Pierre Lescourret/Corbis.
Retro copertina: l'originale Go gopher. © 2009 Renée French. Utilizzato con licenza Creative Commons
Attribuzioni 3.0.
Tipizzato dagli autori in Minion Pro, Lato e Consolas, utilizzando Go, groff, ghostscript e una serie di altri
strumenti Unix open-source. Le figure sono state create con Google Drawings.
ISBN-13: 978-0-13-419044-0
ISBN-10: 0-13-419044-0
Testo stampato negli Stati Uniti su carta riciclata presso la RR Donnelley di Crawfordsville, Indiana. Prima
stampa, ottobre 2015

www.it-ebooks.info
Per Leila e Meg

www.it-ebooks.info
Questa pagina è stata lasciata intenzionalmente in bianco

www.it-ebooks.info
Contenuti

Prefazione xi
Le origini del Go xii
Il progetto Go xiii
Organizzazione del libro xv
Dove trovare maggiori informazioni xvi
Ringraziamenti xvii
1. Tutorial 1
1.1. Ciao, mondo 1
1.2. Argomenti della riga di comando 4
1.3. Individuazione di linee duplicate 8
1.4. GIF animate 13
1.5. Recupero di un URL 15
1.6. Recupero simultaneo di URL 17
1.7. Un server Web 19
1.8. Punti deboli 23
2. Struttura del programma 27
2.1. Nomi 27
2.2. Dichiarazioni 28
2.3. Le variabili 30
2.4. Assegnazioni 36
2.5. Dichiarazioni di tipo 39
2.6. Pacchetti e file 41
2.7. Ambito di applicazione 45

vii

www.it-ebooks.info
viii CONTENUTI

3. Tipi di dati di base 51


3.1. Interi 51
3.2. Numeri in virgola mobile 56
3.3. Numeri complessi 61
3.4. Booleani 63
3.5. Corde 64
3.6. Costanti 75
4. Tipi di composito 81
4.1. Array 81
4.2. Fette 84
4.3. Mappe 93
4.4. Strutture 99
4.5. JSON 107
4.6. Modelli di testo e HTML 113
5. Funzioni 119
5.1. Dichiarazioni di funzione 119
5.2. Ricorsione 121
5.3. Valori di ritorno multipli 124
5.4. Errori 127
5.5. Valori della funzione 132
5.6. Funzioni anonime 135
5.7. Funzioni variabili 142
5.8. Chiamate di funzione differite 143
5.9. Panico 148
5.10. Recupero 151
6. Metodi 155
6.1. Dichiarazioni di metodo 155
6.2. Metodi con un ricevitore a puntatore 158
6.3. Composizione di tipi mediante incorporazione di strutture 161
6.4. Valori ed espressioni del metodo 164
6.5. Esempio: Tipo di vettore di bit 165
6.6. Incapsulamento 168
7. Interfacce 171
7.1. Interfacce come contratti 171
7.2. Tipi di interfaccia 174
7.3. Soddisfazione dell'interfaccia 175
7.4. Parsing dei flag con flag.Value 179
7.5. Valori dell'interfaccia 181

7.6. Ordinamento con sort.Interface 186


7.7. L'interfaccia http.Handler 191
7.8. L'interfaccia degli errori 196
7.9. Esempio: Valutatore di espressioni 197

www.it-ebooks.info
viii CONTENUTI

7.10. Asserzioni sui tipi 205


7.11. Discriminare gli errori con le asserzioni di tipo 206
7.12. Interrogare i comportamenti con le asserzioni sui tipi di interfaccia 208
7.13. Tipo Interruttori 210
7.14. Esempio: Decodifica XML basata su token 213
7.15. Qualche consiglio 216
8. Goroutine e canali 217
8.1. Goroutines 217
8.2. Esempio: Server orologio concorrente 219
8.3. Esempio: Server Eco concorrente 222
8.4. Canali 225
8.5. Looping in parallelo 234
8.6. Esempio: Web Crawler concorrente 239
8.7. Multiplexing con selezione 244
8.8. Esempio: Attraversamento concomitante di una directory 247
8.9. Cancellazione 251
8.10. Esempio: Server di chat 253
9. Concorrenza con variabili condivise 257
9.1. Condizioni di gara 257
9.2. Esclusione reciproca: sync.Mutex 262
9.3. Mutex di lettura/scrittura: sync.RWMutex 266
9.4. Sincronizzazione della memoria 267
9.5. Inizializzazione pigra: sync.Once 268
9.6. Il rilevatore di razza 271
9.7. Esempio: Cache concorrente non bloccante 272
9.8. Goroutine e filettature 280
10. Pacchetti e strumento Go 283
10.1. Introduzione 283
10.2. Importazione di percorsi 284
10.3. La dichiarazione del pacchetto 285
10.4. Dichiarazioni di importazione 285
10.5. Blank Imports 286
10.6. Pacchetti e nomi 289
10.7. Lo strumento Go 290

11. Test 301


11.1. Lo strumento go test 302
11.2. Funzioni di test 302
11.3. Copertura 318
11.4. Funzioni di benchmark 321
11.5. Profilazione 323
11.6. Funzioni di esempio 326
12. Riflessione 329
12.1. Perché la riflessione? 329

www.it-ebooks.info
viii CONTENUTI

12.2. reflect.Type e reflect.Value 330


12.3. Display, una stampante di valori ricorsiva 333
12.4. Esempio: Codifica di espressioni S 338
12.5. Impostazione di variabili con reflect.Value 341
12.6. Esempio: Decodifica di espressioni S 344
12.7. Accesso ai tag di campo delle strutture 348
12.8. Visualizzazione dei metodi di un tipo 351
12.9. Una parola di cautela 352
13. Programmazione a basso livello 353
13.1. unsafe.Sizeof, Alignof e Offsetof 354
13.2. unsafe.Pointer 356
13.3. Esempio: Equivalenza profonda 358
13.4. Chiamare il codice C con cgo 361
13.5. Un'altra parola di cautela 366
Indice 367

www.it-ebooks.info
Prefazione

''Go è un linguaggio di programmazione open source che rende facile costruire software semplice,
affidabile ed efficiente''. (Dal sito web di Go, golang.org)

Go è stato concepito nel settembre 2007 da Robert Griesemer, Rob Pike e Ken Thompson, tutti di
Google, ed è stato annunciato nel novembre 2009. Gli obiettivi del linguaggio e degli strumenti che lo
accompagnano erano l'espressività, l'efficienza nella compilazione e nell'esecuzione e l'efficacia nella
scrittura di programmi affidabili e robusti.

Go ha una somiglianza superficiale con il C e, come quest'ultimo, è uno strumento per programmatori
professionisti, che ottiene il massimo effetto con il minimo dei mezzi. Ma è molto di più di una versione
aggiornata di
C. Prende in prestito e adatta buone idee da molti altri linguaggi, evitando però le caratteristiche che
hanno portato a complessità e codice inaffidabile. Le sue strutture per la concorrenza sono nuove ed
efficienti e il suo approccio all'astrazione dei dati e alla programmazione orientata agli oggetti è
insolitamente flessibile. È dotato di gestione automatica della memoria o di garbage collection.

Go è particolarmente adatto per la costruzione di infrastrutture come i server in rete e di strumenti e


sistemi per i programmatori, ma è davvero un linguaggio di uso generale e trova impiego in ambiti
diversi come la grafica, le applicazioni mobili e l'apprendimento automatico. È diventato popolare come
sostituto dei linguaggi di scripting non tipizzati perché bilancia l'espressività con la sicurezza: I
programmi Go vengono in genere eseguiti più velocemente di quelli scritti in linguaggi dinamici e
subiscono molti meno crash dovuti a errori di tipo inaspettati.

Go è un progetto open-source, quindi il codice sorgente del compilatore, delle librerie e degli strumenti è
liberamente disponibile per chiunque. I contributi al progetto provengono da una comunità attiva a
livello mondiale. Go funziona su sistemi Unix-like, Linux, FreeBSD, OpenBSD, Mac OS X e su Plan 9 e
Microsoft Windows. I programmi scritti in uno di questi ambienti generalmente funzionano senza
modifiche sugli altri.

xi

www.it-ebooks.info
xii PREFAZIO
NE

Questo libro si propone di aiutarvi a iniziare subito a usare Go in modo efficace e a usarlo bene,
sfruttando appieno le caratteristiche del linguaggio e le librerie standard di Go per scrivere programmi
chiari, idiomatici ed efficienti.

Le origini del Go

Come le specie biologiche, le lingue di successo generano una progenie che incorpora i vantaggi dei
propri antenati; gli incroci a volte portano a sorprendenti punti di forza e, molto occasionalmente, nasce
una nuova caratteristica radicale senza precedenti. Osservando queste influenze possiamo imparare
molto sul perché una lingua è così com'è e su quale ambiente si è adattata.
La figura seguente mostra le influenze più importanti dei precedenti linguaggi di programmazione sulla
progettazione di Go.

Go viene talvolta descritto come un "linguaggio simile al C" o come un "C per il 21° secolo". Dal C, Go ha
ereditato la sintassi delle espressioni, le dichiarazioni di flusso di controllo, i tipi di dati di base, il
passaggio di parametri call-by-value, i puntatori e, soprattutto, l'enfasi del C sui programmi che si
compilano in codice macchina efficiente e che cooperano naturalmente con le astrazioni degli attuali
sistemi operativi.

www.it-ebooks.info
LE ORIGINI DEL GO xiii

Ma ci sono altri antenati nell'albero genealogico di Go. Uno dei principali flussi di influenza proviene dai
linguaggi di Niklaus Wirth, a partire da Pascal. Modula-2 ha ispirato il concetto di pacchetto. Oberon
eliminò la distinzione tra file di interfaccia del modulo e file di implementazione del modulo. Oberon-2
ha influenzato la sintassi dei pacchetti, delle importazioni e delle dichiarazioni, mentre Object Oberon
ha fornito la sintassi per le dichiarazioni dei metodi.
Un'altra discendenza tra gli antenati di Go, che lo distingue tra i linguaggi di programmazione recenti, è
una sequenza di linguaggi di ricerca poco conosciuti sviluppati presso i Bell Labs, tutti ispirati al
concetto di processi sequenziali comunicanti (CSP), contenuto nel fondamentale articolo di Tony Hoare
del 1978 sui fondamenti della concorrenza. In CSP, un programma è una composizione parallela di
processi che non hanno uno stato condiviso; i processi comunicano e si sincronizzano utilizzando canali.
Ma il CSP di Hoare era un linguaggio formale per descrivere i concetti fondamentali della concorrenza,
non un linguaggio di programmazione per scrivere programmi eseguibili.
Rob Pike e altri hanno iniziato a sperimentare implementazioni di CSP come linguaggi veri e propri. Il
primo si chiamava Squeak (''A language for communicating with mice''), che forniva un linguaggio per
gestire gli eventi del mouse e della tastiera, con canali creati staticamente. Seguì Newsqueak, che offriva
una sintassi delle dichiarazioni e delle espressioni simile a quella del C e una notazione dei tipi simile a
quella del Pascal. Si trattava di un linguaggio puramente funzionale con garbage collection, sempre
finalizzato alla gestione degli eventi di tastiera, mouse e finestra. I canali divennero valori di prima classe,
creati dinamicamente e memorizzabili in variabili.
Il sistema operativo Plan 9 ha portato avanti queste idee in un linguaggio chiamato Alef. Alef cercava d i
rendere Newsqueak un linguaggio di programmazione di sistema valido, ma la sua omissione della
raccolta dei rifiuti rendeva la concomitanza troppo dolorosa.
Altri costrutti di Go mostrano qua e là l'influenza di geni non ancestrali; ad esempio iota deriva
vagamente da APL, mentre l'ambito lessicale con funzioni annidate deriva da Scheme (e dalla
maggior parte dei linguaggi successivi). Anche qui troviamo nuove mutazioni. Le innovative slices di Go
forniscono array dinamici con un efficiente accesso casuale, ma permettono anche sofisticate
disposizioni di condivisione che ricordano le liste collegate. Anche l'istruzione defer è una novità di
Go.

Il progetto Go

Tutti i linguaggi di programmazione riflettono la filosofia di programmazione dei loro creatori, che
spesso include una componente significativa di reazione alle carenze percepite dei linguaggi precedenti.
Il progetto Go è nato dalla frustrazione per diversi sistemi software di Google che soffrivano di
un'esplosione di complessità. (Questo problema non è affatto unico per Google).
Come ha detto Rob Pike, "la complessità è moltiplicativa": risolvere un problema rendendo più
complessa una parte del sistema aggiunge lentamente ma inesorabilmente complessità ad altre parti.
Con la costante preoccupazione di aggiungere funzionalità, opzioni e configurazioni e di spedire il
codice in tempi brevi, è facile trascurare la semplicità, anche se a lungo termine la semplicità è la chiave
di un buon software.

www.it-ebooks.info
xiv PREFAZIO
NE

La semplicità richiede più lavoro all'inizio del progetto per ridurre un'idea alla sua essenza e più
disciplina nel corso della vita del progetto per distinguere le modifiche buone da quelle cattive o
perniciose. Con uno sforzo sufficiente, una buona modifica può essere accolta senza compromettere
quella che Fred Brooks chiamava l'"integrità concettuale" del progetto, ma una cattiva modifica non può
farlo, e una modifica perniciosa scambia la semplicità con la sua cugina più superficiale, la convenienza.
Solo attraverso la semplicità della progettazione un sistema può rimanere stabile, sicuro e coerente
durante la sua crescita.
Il progetto Go comprende il linguaggio stesso, i suoi strumenti e le librerie standard e, ultimo ma non
meno importante, un programma culturale di radicale semplicità. Essendo un linguaggio di alto livello
recente, Go ha il vantaggio del senno di poi e le basi sono ben fatte: ha la garbage collection, un sistema
di pacchetti, funzioni di prima classe, scope lessicale, un'interfaccia per le chiamate di sistema e stringhe
immutabili in cui il testo è generalmente codificato in UTF-8. Ma ha relativamente poche funzionalità ed
è improbabile che ne aggiunga altre. Ma ha relativamente poche funzionalità ed è improbabile che ne
aggiunga altre. Per esempio, non ha conversioni numeriche implicite, né costruttori o distruttori, né
sovraccarico degli operatori, né valori predefiniti dei parametri, né ereditarietà, né generici, né eccezioni,
né macro, né annotazioni di funzioni, né archiviazione thread-local. Il linguaggio è maturo e stabile e
garantisce la compatibilità all'indietro: i vecchi programmi Go possono essere compilati ed eseguiti con
le nuove versioni di compilatori e librerie standard.
Go ha un sistema di tipi sufficiente a evitare la maggior parte degli errori incauti che affliggono i
programmatori nei linguaggi dinamici, ma ha un sistema di tipi più semplice rispetto a linguaggi
tipizzati simili. Questo approccio può talvolta portare a sacche isolate di programmazione "non tipizzata"
all'interno di un quadro più ampio di tipi, e i programmatori di Go non si spingono come quelli di C++
o Haskell a esprimere le proprietà di sicurezza come prove basate sui tipi. In pratica, però, Go offre ai
programmatori molti dei vantaggi in termini di sicurezza e prestazioni di esecuzione d i un sistema di
tipi relativamente forte, senza l'onere di un sistema complesso.
Go incoraggia la consapevolezza della progettazione dei sistemi informatici contemporanei, in
particolare dell'importanza della localizzazione. I suoi tipi di dati incorporati e la maggior parte delle
strutture di dati della libreria sono realizzati in modo da funzionare in modo naturale senza
inizializzazione esplicita o costruttori impliciti, quindi nel codice sono nascoste relativamente poche
allocazioni e scritture di memoria. I tipi aggregati di Go (struct e array) contengono direttamente i loro
elementi, richiedendo meno memoria e meno allocazioni e indirezioni di puntatori rispetto ai linguaggi
che utilizzano campi indiretti. Inoltre, poiché il computer moderno è una macchina par- ticolare, Go
dispone di funzioni di concorrenza basate su CSP, come accennato in precedenza. Gli stack di
dimensioni variabili dei thread leggeri o delle goroutine di Go sono inizialmente abbastanza piccoli da
rendere economica la creazione di una goroutine e pratica la creazione di un milione.
La libreria standard di Go, spesso descritta come dotata di "batterie incluse", fornisce blocchi di
costruzione e API pulite per l'I/O, l'elaborazione del testo, la grafica, la crittografia, la rete e le
applicazioni distribuite, con il supporto di molti formati di file e protocolli standard. Le librerie e gli
strumenti fanno ampio uso di convenzioni per ridurre la necessità di configurazioni e spiegazioni,
semplificando così la logica dei programmi e rendendo i diversi programmi Go più simili tra loro e
quindi più facili da imparare. I progetti costruiti con lo strumento go utilizzano solo i nomi dei file e
degli identificatori e un occasionale commento speciale per determinare tutte le librerie, gli eseguibili, i
test, i benchmark, gli esempi, le varianti specifiche per la piattaforma e la documentazione di un
progetto; il sorgente Go stesso contiene le specifiche di compilazione.

www.it-ebooks.info
IL PROGETTO GO xv

Organizzazione del libro

Si presume che abbiate programmato in uno o più altri linguaggi, sia compilati come C, C++ e Java, sia
interpretati come Python, Ruby e JavaScript, quindi non spiegheremo ogni cosa come se fosse per un
principiante. La sintassi superficiale sarà familiare, così come le variabili e le costanti, le espressioni, il
flusso di controllo e le funzioni.
Il capitolo 1 è un tutorial sui costrutti di base di Go, introdotti attraverso una dozzina di programmi per
attività quotidiane come la lettura e la scrittura di file, la formattazione del testo, la creazione di
immagini e la comunicazione con client e server Internet.
Il capitolo 2 descrive gli elementi strutturali di un programma Go: dichiarazioni, variabili, nuovi
tipi, pacchetti e file e ambito. Il capitolo 3 parla di numeri, booleani, stringhe e con- stanti e spiega
come elaborare Unicode. Il capitolo 4 descrive i tipi compositi, cioè i tipi costruiti a partire da quelli
più semplici utilizzando array, mappe, strutture e slices, l'approccio di Go agli elenchi dinamici. Il
capitolo 5 tratta le funzioni e discute la gestione degli errori, il panico e il recupero e l'istruzione defer.
I capitoli da 1 a 5 sono quindi le basi, cose che fanno parte di qualsiasi linguaggio imperativo
mainstream. La sintassi e lo stile di Go a volte differiscono da altri linguaggi, ma la maggior parte dei
programmatori li apprenderà rapidamente. I restanti capitoli si concentrano su argomenti in cui
l'approccio di Go è meno convenzionale: metodi, interfacce, concorrenza, pacchetti, test e riflessione.
Go ha un approccio insolito alla programmazione orientata agli oggetti. Non ci sono gerarchie di classi, o
addirittura nessuna classe; i comportamenti complessi degli oggetti sono creati da quelli più semplici per
composizione, non per ereditarietà. I metodi possono essere associati a qualsiasi tipo definito dall'utente,
non solo alle strutture, e la relazione tra tipi concreti e tipi astratti (interfacce) è implicita, quindi un tipo
concreto può soddisfare un'interfaccia di cui il progettista del tipo non era a conoscenza. I metodi sono
t r a t t a t i nel Capitolo 6, mentre le interfacce nel Capitolo 7.
Il capitolo 8 presenta l'approccio di Go alla concorrenza, che si basa sull'idea di processi sequenziali
comunicanti (CSP), incarnati da goroutine e canali. Il capitolo 9 illustra gli aspetti più tradizionali della
concorrenza basata sulle variabili condivise.
Il Capitolo 10 descrive i pacchetti, il meccanismo di organizzazione delle librerie. Questo capitolo mostra
anche come utilizzare in modo efficace lo strumento go, che consente di compilare, testare, eseguire
benchmark, formattare i programmi, documentare e svolgere molte altre attività, il tutto con un unico
comando.
Il capitolo 11 tratta del testing, dove Go adotta un approccio notevolmente leggero, evitando framework
carichi di astrazioni a favore di semplici librerie e strumenti. Le librerie di test forniscono una base su cui
costruire astrazioni più complesse, se necessario.
Il capitolo 12 tratta della riflessione, la capacità di un programma di esaminare la propria
rappresentazione durante l'esecuzione. La riflessione è uno strumento potente, ma da usare con
attenzione; questo capitolo spiega come trovare il giusto equilibrio mostrando come viene usata per
implementare alcune importanti librerie di Go. Il capitolo 13 spiega i dettagli cruenti della
programmazione di basso livello che utilizza il pacchetto unsafe per aggirare il sistema di tipi di Go, e
quando ciò è appropriato.

www.it-ebooks.info
xvi PREFAZIO
NE

Ogni capitolo contiene una serie di esercizi che possono essere utilizzati per verificare la comprensione
di Go e per esplorare estensioni e alternative agli esempi del libro.
Tutti gli esempi di codice del libro, tranne quelli più banali, sono disponibili per il download dal
repository Git pubblico all'indirizzo gopl.io. Ogni esempio è identificato dal percorso di importazione
del pacchetto e può essere comodamente recuperato, costruito e installato con il comando go get. È
necessario scegliere una directory come spazio di lavoro di Go e impostare la variabile d'ambiente
GOPATH in modo che vi punti. Lo strumento go creerà la directory, se necessario. Ad esempio:
$ export GOPATH=$HOME/gobook # scegliere la directory dello spazio di lavoro
$ go get gopl.io/ch1/helloworld # recupera, costruisce, installa
$ $GOPATH/bin/helloworld #
eseguire Hello, B$

Per eseguire gli esempi, è necessaria almeno la versione 1.5 di Go.


$ go version
go versione go1.5 linux/amd64

Seguire le istruzioni all'indirizzo https://golang.org/doc/install se lo strumento Go del computer è


vecchio o mancante.

Dove trovare maggiori informazioni

La migliore fonte di informazioni su Go è il sito web ufficiale, https://golang.org, che fornisce l'accesso
alla documentazione, comprese le specifiche del linguaggio di programmazione Go, i pacchetti standard e
simili. Ci sono anche tutorial su come scrivere Go e su come scriverlo bene, e un'ampia varietà di risorse
testuali e video online che saranno un valido complemento a questo libro. Il Go Blog all'indirizzo
blog.golang.org pubblica alcuni dei migliori scritti su Go, con articoli sullo stato del linguaggio, piani
per il futuro, resoconti di conferenze e spiegazioni approfondite di un'ampia varietà di argomenti legati a
Go.
Uno degli aspetti più utili dell'accesso online a Go (e una spiacevole limitazione di un libro cartaceo) è la
possibilità di eseguire programmi Go dalle pagine web che li descrivono. Questa funzionalità è fornita
dal Go Playground all'indirizzo play.golang.org e può essere incorporata in altre pagine, come la
home page di golang.org o le pagine di documentazione servite dallo strumento godoc.
Il Playground consente di eseguire semplici esperimenti per verificare la propria conoscenza della
sintassi, della semantica o dei pacchetti di librerie con brevi programmi e, per molti versi, prende il posto
di un ciclo read-eval-print (REPL) in altri linguaggi. I suoi URL persistenti sono ottimi per condividere
frammenti di codice Go con altri, per segnalare bug o dare suggerimenti.
Costruito sulla base del Playground, il Go Tour, all'indirizzo tour.golang.org, è una sequenza di brevi
lezioni interattive sulle idee e i costrutti di base di Go, una passeggiata ordinata attraverso il linguaggio.
Il principale difetto del Playground e del Tour è che permettono di importare solo librerie standard e
molte funzioni delle librerie, come ad esempio la rete, sono limitate.

www.it-ebooks.info
DOVE TROVARE MAGGIORI INFORMAZIONI xvii

per motivi pratici o di sicurezza. Inoltre, richiedono l'accesso a Internet per compilare ed eseguire
ogni programma. Quindi, per gli esperimenti più elaborati, dovrete eseguire i programmi Go sul vostro
computer. Fortunatamente il processo di download è semplice, quindi non ci vorranno più di pochi
minuti per scaricare la distribuzione Go da golang.org e iniziare a scrivere ed eseguire i propri
programmi Go.
Poiché Go è un progetto open-source, è possibile leggere il codice di qualsiasi tipo o funzione della
libreria stan- dard online all'indirizzo https://golang.org/pkg; lo stesso codice fa parte della
distribuzione scaricata. Utilizzatelo per capire come funziona qualcosa, per rispondere a
domande sui dettagli o semplicemente per vedere come gli esperti scrivono un ottimo Go.

Ringraziamenti

Rob Pike e Russ Cox, membri fondamentali del team di Go, hanno letto il manoscritto più volte con
grande attenzione; i loro commenti su tutto, dalla scelta delle parole alla struttura e all'organizzazione
generale, sono stati preziosi. Durante la preparazione della traduzione giapponese, Yoshiki Shibata è
andato ben oltre il suo dovere; il suo occhio meticoloso ha individuato numerose incongruenze nel testo
inglese ed errori nel codice. Abbiamo apprezzato molto le revisioni approfondite e i commenti critici
sull'intero manoscritto da parte di Brian Goetz, Corey Kosak, Arnold Robbins, Josh Bleecher Snyder e
Peter Weinberger.
Siamo grati a Sameer Ajmani, Ittai Balaban, David Crawshaw, Billy Donohue, Jonathan Feinberg,
Andrew Gerrand, Robert Griesemer, John Linderman, Minux Ma, Bryan Mills, Bala Natarajan, Cosmos
Nicolaou, Paul Staniforth, Nigel Tao e Howard Trickey per i numerosi suggerimenti utili. Ringraziamo
anche David Brailsford e Raph Levien per la consulenza tipografica.
Il nostro editore Greg Doench di Addison-Wesley ha dato il via all'iniziativa iniziale e da allora è stato
sempre di grande aiuto. Il team di produzione dell'AW - John Fuller, Dayna Isley, Julie Nahil, Chuti
Prasertsith e Barbara Wood - è stato eccezionale; gli autori non potevano sperare in un supporto
migliore.
Alan Donovan desidera ringraziare: Sameer Ajmani, Chris Demetriou, Walt Drummond e Reid Tatge di
Google per avergli concesso il tempo di scrivere; Stephen Donovan, per i suoi consigli e il suo tempestivo
incoraggiamento; e soprattutto sua moglie Leila Kazemi, per il suo incrollabile entusiasmo e il suo
incrollabile sostegno a questo progetto, nonostante le lunghe ore di distrazione e di assenza dalla vita
familiare che ha comportato.
Brian Kernighan è profondamente grato ad amici e colleghi per la loro pazienza e sopportazione mentre
si muoveva lentamente lungo il cammino verso la comprensione, e soprattutto a sua moglie Meg, che è
stata immancabilmente di supporto nella scrittura del libro e in molte altre cose.
New York
ottobre 2015

www.it-ebooks.info
Questa pagina è stata lasciata intenzionalmente in bianco

www.it-ebooks.info
1
Tutorial

Questo capitolo è un tour dei componenti di base di Go. Speriamo di fornire informazioni ed esempi
sufficienti a farvi iniziare a fare cose utili il più rapidamente possibile. Gli esempi qui, e in effetti in tutto
il libro, sono rivolti a compiti che potreste dover svolgere nel mondo reale. In questo capitolo
cercheremo di dare un assaggio della varietà di programmi che si possono scrivere in Go, dalla semplice
elaborazione di file e un po' di grafica ai client e ai server Internet simultanei. Sicuramente non
spiegheremo tutto nel primo capitolo, ma studiare tali programmi in un nuovo linguaggio può essere un
modo efficace per iniziare.
Quando si impara un nuovo linguaggio, c'è una tendenza naturale a scrivere il codice come lo si sarebbe
scritto in un linguaggio che si conosce già. Durante l'apprendimento di Go, bisogna essere consapevoli di
questa tendenza e cercare di evitarla. Abbiamo cercato di illustrare e spiegare come scrivere un buon Go,
quindi usate il codice qui come guida quando scrivete il vostro.

1.1. Ciao, mondo

Inizieremo con l'ormai tradizionale esempio di ''hello, world'', che compare all'inizio di The C
Programming Language, pubblicato nel 1978. Il C è una delle influenze più dirette di Go e ''hello, world''
illustra una serie di idee centrali.
gopl.io/ch1/helloworld
pacchetto principale

importare "fmt"

func main() {
fmt.Println("Ciao, B$")
}

www.it-ebooks.info
2 CAPITOLO 1. TUTORIAL

Go è un linguaggio compilato. La toolchain di Go converte un programma sorgente e gli elementi da cui


dipende in istruzioni nel linguaggio macchina nativo di un computer. A questi strumenti si accede
attraverso un singolo comando chiamato go, che ha una serie di sottocomandi. Il più semplice di questi
sottocomandi è run, che compila il codice sorgente da uno o più file sorgente il cui nome termina con .go,
lo collega con le librerie, quindi esegue il file eseguibile risultante. (Nel libro useremo $ come prompt dei
comandi).
$ go run helloworld.go

Non sorprende che questa stampa


Ciao, B$

Go gestisce in modo nativo Unicode, quindi può elaborare testi in tutte le lingue del mondo.
Se il programma è qualcosa di più di un esperimento singolo, è probabile che si voglia compilarlo una
volta e salvare il risultato compilato per un uso successivo. Questo si può fare con go build:
$ go build helloworld.go

Questo crea un file binario eseguibile chiamato helloworld che può essere eseguito in qualsiasi
momento senza ulteriori elaborazioni:
$ ./helloworld
Ciao, B$

Abbiamo etichettato ogni esempio significativo per ricordare che è possibile ottenere il codice dal
repository del codice sorgente del libro all'indirizzo gopl.io:
gopl.io/ch1/helloworld

Se si esegue go get gopl.io/ch1/helloworld, si recupera il codice sorgente e lo si colloca nella


directory corrispondente. Per ulteriori informazioni su questo argomento, si veda la Sezione 2.6 e la
Sezione 10.7.
Parliamo ora del programma stesso. Il codice Go è organizzato in pacchetti, simili alle librerie o ai
moduli di altri linguaggi. Un pacchetto è costituito da uno o più file sorgenti .go in una singola
directory che definiscono le funzioni del pacchetto. Ogni file sorgente inizia con una dichiarazione di
pacchetto, qui package main, che indica a quale pacchetto appartiene il file, seguita da un elenco di altri
pacchetti che importa e quindi dalle dichiarazioni del programma che sono contenute nel file.
La libreria standard di Go contiene oltre 100 pacchetti per compiti comuni come l'input e l'output,
l'ordinamento e la manipolazione del testo. Ad esempio, il pacchetto fmt contiene funzioni per la stampa
di output formattati e la scansione di input. Println è una delle funzioni di output di base di fmt; stampa
uno o più valori, separati da spazi, con un carattere newline alla fine, in modo che i valori appaiano
come una singola riga di output.
Il pacchetto main è speciale. Definisce un programma eseguibile autonomo, non una libreria. All'interno
del pacchetto main, anche la funzione main è speciale: è il punto in cui inizia l'esecuzione del programma.
Qualsiasi cosa faccia main è ciò che fa il programma. Naturalmente, main richiama normalmente funzioni
di altri pacchetti per svolgere gran parte del lavoro, come la funzione fmt.Println.

www.it-ebooks.info
SEZIONE 1.1. CIAO, MONDO 3

Dobbiamo dire al compilatore quali pacchetti sono necessari per questo file sorgente; questo è il ruolo
della dichiarazione di importazione che segue la dichiarazione del pacchetto. Il programma ''hello, world''
utilizza solo una funzione di un altro pacchetto, ma la maggior parte dei programmi importerà più
pacchetti.

È necessario importare esattamente i pacchetti necessari. Un programma non verrà compilato se


mancano importazioni o se ve ne sono di non necessarie. Questo requisito rigoroso impedisce che i
riferimenti ai pacchetti inutilizzati si accumulino durante l'evoluzione dei programmi.

Le dichiarazioni di importazione devono seguire la dichiarazione del pacchetto. Dopodiché, un


programma consiste nelle dichiarazioni di funzioni, variabili, costanti e tipi (introdotte dalle parole
chiave func, var, const e type); per la maggior parte, l'ordine delle dichiarazioni non ha importanza.
Questo programma è il più breve possibile, poiché dichiara una sola funzione, che a sua volta chiama
una sola altra funzione. Per risparmiare spazio, a volte non mostriamo le dichiarazioni di package e
import quando presentiamo gli esempi, ma sono presenti nel file sorgente e devono essere presenti per
compilare il codice.

Una dichiarazione di funzione consiste nella parola chiave func, nel nome della funzione, in un elenco di
parametri (vuoto per main), in un elenco di risultati (anch'esso vuoto) e nel corpo della funzione - gli
stati che definiscono ciò che fa - racchiusi tra parentesi graffe. Le funzioni saranno analizzate più da
vicino nel Capitolo 5.

Go non richiede il punto e virgola alla fine delle dichiarazioni, a meno che non ne compaiano due o più
sulla stessa riga. In effetti, le lineette che seguono alcuni token vengono convertite in punti e virgola,
quindi la posizione delle lineette è importante per il corretto parsing del codice Go. Ad esempio, la
parentesi graffa di apertura { della funzione deve trovarsi sulla stessa riga della fine della
dichiarazione func, non su una riga a sé stante, e nell'espressione x + y, è consentita una newline
dopo ma non prima dell'operatore +.

Go adotta una posizione forte sulla formattazione del codice. Lo strumento gofmt riscrive il codice nel
formato standard e il sottocomando fmt dello strumento go applica gofmt a tutti i file del pacchetto
specificato, o a quelli della directory corrente per impostazione predefinita. Tutti i file sorgenti di Go
presenti nel libro sono stati sottoposti a gofmt e si dovrebbe prendere l'abitudine di fare lo stesso per il
proprio codice. Dichiarare un formato standard elimina un sacco di inutili discussioni su questioni
banali e, cosa più importante, consente una serie di trasformazioni automatiche del codice sorgente che
sarebbero impossibili se fosse consentita una formattazione arbitraria.

Molti editor di testo possono essere configurati per eseguire gofmt ogni volta che si salva un file, in modo
che il codice sorgente sia sempre formattato correttamente. Uno strumento correlato, goimports, gestisce
inoltre l'inserimento e la rimozione delle dichiarazioni di importazione secondo le necessità. Non fa
parte della distribuzione standard, ma si può ottenere con questo comando:

$ go get golang.org/x/tools/cmd/goimports

Per la maggior parte degli utenti, il modo abituale per scaricare e compilare i pacchetti, eseguire i test,
mostrare la documentazione e così via, è lo strumento go, che vedremo nella Sezione 10.7.

www.it-ebooks.info
4 CAPITOLO 1. TUTORIAL

1.2. Argomenti della riga di comando

La maggior parte dei programmi elabora alcuni input per produrre alcuni output; questa è praticamente
la definizione di informatica. Ma come fa un programma a ottenere i dati di input su cui operare? Alcuni
programmi generano i propri dati, ma più spesso l'input proviene da una fonte esterna: un file, una
connessione di rete, l'output di un altro programma, un utente alla tastiera, argomenti della riga di
comando o simili. Nei prossimi esempi verranno discusse alcune di queste alternative, a partire dagli
argomenti della riga di comando.

Il pacchetto os fornisce funzioni e altri valori per gestire il sistema operativo in modo indipendente dalla
piattaforma. Gli argomenti della riga di comando sono disponibili p e r un programma in una
variabile chiamata Args che fa parte del pacchetto os; pertanto il suo nome al di fuori del pacchetto os
è os.Args.

La variabile os.Args è una fetta di stringhe. Le slice sono una nozione fondamentale in Go e presto ne
p a r l e r e m o ancora. Per ora, si pensi a una slice come a una sequenza s di elementi di array di
dimensioni dinamiche, in cui si può accedere ai singoli elementi come s[i] e a una sottose- quenza
contigua come s[m:n]. Il numero di elementi è dato da len(s). Come nella maggior parte degli altri
linguaggi di programmazione, in Go tutte le indicizzazioni utilizzano intervalli semiaperti che includono
il primo indice ma escludono l'ultimo, perché questo semplifica la logica. Ad esempio, la slice s[m:n],
dove 0 ≤ m ≤ n ≤ len(s), contiene n-m elementi.

Il primo elemento di os.Args, os.Args[0], è il nome del comando stesso; gli altri elementi sono gli
argomenti presentati al programma all'avvio dell'esecuzione. Un'espressione slice della forma s[m:n]
produce una slice che si riferisce agli elementi da m a n-1, quindi gli elementi di cui abbiamo bisogno per
il prossimo esempio sono quelli della slice os.Args[1:len(os.Args)]. Se m o n vengono omessi, il valore
predefinito è rispettivamente 0 o len(s), quindi possiamo abbreviare la slice desiderata come os.Args[1:].

Ecco un'implementazione del comando Unix echo, che stampa gli argomenti della riga di comando su
una singola riga. Importa due pacchetti, che sono forniti come elenco di parentesi anziché come singole
dichiarazioni di importazione. Entrambe le forme sono legali, ma convenzionalmente si usa la forma a
elenco. L'ordine delle importazioni non ha importanza; lo strumento gofmt ordina i nomi dei pacchetti
in ordine alfabetico. (Quando ci sono diverse versioni di un esempio, spesso le numeriamo per essere
sicuri di quale stiamo parlando).

gopl.io/ch1/echo1
// Echo1 stampa gli argomenti della riga di comando.
pacchetto main

importare (
"fmt"
"os"
)

www.it-ebooks.info
SEZIONE 1.2. ARGOMENTI DELLA RIGA DI 5
COMANDO

func main() {
var s, sep stringa
for i := 1; i < len(os.Args); i++ { s +=
sep + os.Args[i]
sep = " "
}
fmt.Println(s)
}

I commenti iniziano con //. Tutto il testo da // alla fine della riga è un commento per i programmatori e
viene ignorato dal compilatore. Per convenzione, ogni pacchetto viene descritto in un commento che
precede immediatamente la sua dichiarazione; per un pacchetto principale, questo commento è costituito
da una o più frasi complete che descrivono il programma nel suo complesso.
La dichiarazione var dichiara due variabili s e sep, di tipo string. Una variabile può essere inizializzata
come parte della sua dichiarazione. Se non viene inizializzata esplicitamente, viene inizializzata
implicitamente al valore zero per il suo tipo, che è 0 per i tipi numerici e la stringa vuota "" per le
stringhe. In questo esempio, quindi, la dichiarazione inizializza implicitamente s e sep a stringhe vuote.
Parleremo ancora di variabili e dichiarazioni nel capitolo 2.
Per i numeri, Go fornisce i consueti operatori aritmetici e logici. Quando viene applicato alle stringhe,
tuttavia, l'operatore + concatena i valori, quindi l'espressione
sep + os.Args[i]

rappresenta la concatenazione delle stringhe sep e os.Args[i]. L'istruzione utilizzata nel programma,
s += sep + os.Args[i]

è un'istruzione di assegnazione che concatena il vecchio valore di s con sep e os.Args[i] e lo assegna
nuovamente a s; è equivalente a
s = s + sep + os.Args[i]

L'operatore += è un operatore di assegnazione. Ogni operatore aritmetico e logico come + o * ha un


operatore di assegnazione corrispondente.
Il programma echo avrebbe potuto stampare il suo output in un ciclo un pezzo alla volta, ma questa
versione costruisce invece una stringa aggiungendo ripetutamente nuovo testo alla fine. La stringa s
inizia vuota, cioè con il valore "", e a ogni passaggio nel ciclo vi aggiunge del testo; dopo la prima
iterazione, viene inserito anche uno spazio, in modo che al termine del ciclo ci sia uno spazio tra ogni
argomento. Si tratta di un processo quadratico che potrebbe essere costoso se il numero di argomenti è
elevato, ma per echo è improbabile. In questo capitolo e nel prossimo mostreremo una serie di versioni
migliorate di echo che affronteranno qualsiasi inefficienza.
La variabile indice del ciclo i è dichiarata nella prima parte del ciclo for. Il simbolo := fa parte di
una dichiarazione di variabile breve, un'istruzione che dichiara una o più variabili e assegna loro i
tipi appropriati in base ai valori dell'inizializzatore; se ne parlerà nel prossimo capitolo.
L'istruzione di incremento i++ aggiunge 1 a i; è equivalente a i += 1, che a sua volta è equivalente a i = i +
1. Esiste un'istruzione di decremento corrispondente i-- che sottrae 1. Esiste un'istruzione di
decremento corrispondente, i--, che sottrae 1. Questi sono

www.it-ebooks.info
6 CAPITOLO 1. TUTORIAL

non espressioni come nella maggior parte dei linguaggi della famiglia C, quindi j = i++ è illegale, e sono
solo postfissi, quindi nemmeno --i è legale.

Il ciclo for è l'unica istruzione di ciclo in Go. Ha diverse forme, una delle quali è illustrata qui:
per inizializzazione; condizione; post {
// zero o più dichiarazioni
}

Le parentesi non vengono mai usate intorno ai tre componenti di un ciclo for. Tuttavia, le parentesi sono
obbligatorie e la parentesi di apertura deve trovarsi sulla stessa riga dell'istruzione post.

L'istruzione di inizializzazione opzionale viene eseguita prima dell'avvio del ciclo. Se è presente,
deve essere un'istruzione semplice, cioè una breve dichiarazione di variabile, un'istruzione di incremento
o assegnazione, o una chiamata di funzione. La condizione è un'espressione booleana che viene valutata
all'inizio di ogni iterazione del ciclo; se è vera, vengono eseguite le istruzioni controllate dal ciclo.
L'istruzione post viene eseguita dopo il corpo del ciclo, quindi la condizione viene valutata di nuovo. Il
ciclo termina quando la condizione diventa falsa.

Ciascuna di queste parti può essere omessa. Se non c'è inizializzazione e non c'è post, anche i
semi-colon possono essere omessi:
// un ciclo "while" tradizionale per
la condizione {
// ...
}

Se la condizione è completamente omessa in una di queste forme, ad esempio in


// un ciclo infinito tradizionale
per {
// ...
}

il ciclo è infinito, anche se i loop di questa forma possono essere terminati in altro m o d o , come un
dichiarazione di interruzione o di ritorno.

Un'altra forma di ciclo for itera su un intervallo di valori di un tipo di dati come una stringa o una slice.
Per illustrare, ecco una seconda versione di echo:

gopl.io/ch1/echo2
// Echo2 stampa gli argomenti della riga di comando.
pacchetto main

importare (
"fmt"
"os"
)

www.it-ebooks.info
SEZIONE 1.2. ARGOMENTI DELLA RIGA DI 7
COMANDO

func main() {
s, sep := "", ""
per _, arg := range os.Args[1:] { s +=
sep + arg
sep = " "
}
fmt.Println(s)
}

In ogni iterazione del ciclo, range produce una coppia di valori: l'indice e il valore dell'elemento a
quell'indice. In questo esempio, non abbiamo bisogno dell'indice, ma la sintassi di un ciclo range richiede
che se trattiamo l'elemento, dobbiamo trattare anche l'indice. Un'idea potrebbe essere quella di assegnare
l'indice a una variabile temporanea come temp e ignorare il suo valore, ma Go non consente di utilizzare
variabili locali inutilizzate, per cui si otterrebbe un errore di compilazione.

La soluzione è usare l'identificatore vuoto, il cui nome è _ (cioè un trattino basso). L'identificatore blank
può essere utilizzato ogni volta che la sintassi richiede il nome di una variabile ma la logica del
programma no, ad esempio per scartare un indice di ciclo indesiderato quando si richiede solo il valore
dell'elemento. La maggior parte dei programmatori di Go userebbe probabilmente range e _ per scrivere
il programma echo come sopra, poiché l'indicizzazione su os.Args è implicita, non esplicita, e quindi più
facile da ottenere.

Questa versione del programma utilizza una breve dichiarazione di variabile per dichiarare e
inizializzare s e sep, ma avremmo potuto benissimo dichiarare le variabili separatamente. Esistono
diversi modi per dichiarare una variabile stringa; sono tutti equivalenti:

s := ""
var s stringa var
s = ""
var s stringa = ""

Perché preferire una forma all'altra? La prima forma, una breve dichiarazione di variabile, è la più
compatta, ma può essere usata solo all'interno di una funzione, non per le variabili a livello di pacchetto.
La seconda forma si basa sull'inizializzazione predefinita al valore zero per le stringhe, che è "". La terza
forma è usata raramente, tranne quando si dichiarano più variabili. La quarta forma prevede
l'esplicitazione del tipo di variabile, che è superfluo quando è uguale a quello del valore iniziale, ma
necessario negli altri casi in cui non sono dello stesso tipo. In pratica, si dovrebbe generalmente
utilizzare una delle prime due forme, con l'inizializzazione esplicita per dire che il valore iniziale è
importante e l'inizializzazione implicita per dire che il valore iniziale non è importante.

Come si è detto in precedenza, a ogni ciclo la stringa s riceve un contenuto completamente nuovo.
L'istruzione += crea una nuova stringa concatenando la vecchia stringa, un carattere di spazio e
l'argomento successivo, quindi assegna la nuova stringa a s. I vecchi contenuti di s non sono più in uso,
quindi saranno raccolti a tempo debito.

Se la quantità di dati coinvolti è elevata, questa operazione potrebbe essere costosa. Una soluzione
più semplice ed efficiente sarebbe quella di utilizzare la funzione Join del pacchetto strings:

www.it-ebooks.info
8 CAPITOLO 1. TUTORIAL

gopl.io/ch1/echo3
func main() {
fmt.Println(strings.Join(os.Args[1:], " "))
}

Infine, se non ci interessa il formato ma vogliamo solo vedere i valori, magari per il debug, possiamo
lasciare che Println formatti i risultati per noi:
fmt.Println(os.Args[1:])

L'output di questa istruzione è simile a quello che si ottiene da strings.Join, ma con le parentesi di
arrotondamento. In questo modo è possibile stampare qualsiasi slice.

Esercizio 1.1: Modificate il programma echo per stampare anche os.Args[0], il nome del comando che lo
ha invocato.

Esercizio 1.2: Modificate il programma echo per stampare l'indice e il valore di ciascuno dei suoi
argomenti, uno per riga.

Esercizio 1.3: Esperimento per misurare la differenza di tempo di esecuzione tra le nostre versioni
potenzialmente inefficienti e quella che utilizza strings.Join. (La Sezione 1.6 illustra parte del pacchetto
tempo e la Sezione 11.4 mostra come scrivere test di benchmark per una valutazione sistematica delle
prestazioni).

1.3. Trovare linee duplicate

I programmi per la copia di file, la stampa, la ricerca, l'ordinamento, il conteggio e simili hanno tutti una
struttura simile: un ciclo sull'input, qualche calcolo su ogni elemento e la generazione di output al volo o
alla fine. Mostreremo tre varianti di un programma chiamato dup; si ispira in parte al comando Unix
uniq, che cerca le righe adiacenti duplicate. Le strutture e i pacchetti utilizzati sono modelli che possono
essere facilmente adattati.

La prima versione di dup stampa ogni riga che appare più di una volta nello standard input,
preceduta dal suo numero. Questo programma introduce l'istruzione if, il tipo di dati map e il
pacchetto bufio.
gopl.io/ch1/dup1
// Dup1 stampa il testo di ogni riga che appare più di una volta.
// una volta nello standard input, preceduto dal suo conteggio.
pacchetto main

importare (
"bufio"
"fmt"
"os"
)

www.it-ebooks.info
SEZIONE 1.3. RICERCA DI LINEE DUPLICATE 9

func main() {
counts := make(map[string]int) input :=
bufio.NewScanner(os.Stdin) for
input.Scan() {
conta[input.Text()]++
}
// NOTA: ignorare i potenziali errori di input.Err() per
line, n := range counts {
se n > 1 {
fmt.Printf("%d\t%s\n", n, riga)
}
}
}

Come nel caso di for, le parentesi non vengono mai usate intorno alla condizione in un'istruzione
if, ma le parentesi graffe sono necessarie per il corpo. Può essere presente una parte opzionale else
che viene eseguita se la condizione è falsa.
Una mappa contiene un insieme di coppie chiave/valore e fornisce operazioni a tempo costante per
memorizzare, recuperare o verificare un elemento dell'insieme. La chiave può essere di qualsiasi tipo i
cui valori possono essere confrontati con ==, le stringhe sono l'esempio più comune; il valore può essere
di qualsiasi tipo. In questo esempio, le chiavi sono stringhe e i valori sono ints. La funzione incorporata
make crea una nuova mappa vuota; ha anche altri usi. Le mappe sono trattate in dettaglio nella Sezione
4.3.
Ogni volta che dup legge una riga di input, la riga viene usata come chiave nella mappa e il valore di
risposta viene incrementato. L'istruzione counts[input.Text()]++ è equivalente a queste due istruzioni:
riga := input.Text() conteggi[riga] =
conteggi[riga] + 1

Non è un problema se la mappa non contiene ancora quella chiave. La prima volta che viene vista una
nuova riga, l'espressione counts[line] sul lato destro valuta il valore zero per il suo tipo, che è 0 per int.
Per stampare i risultati, utilizziamo un altro ciclo for basato sull'intervallo, questa volta sulla mappa dei
conteggi. Come in precedenza, ogni iterazione produce due risultati, una chiave e il valore dell'elemento
della mappa per quella chiave. L'ordine di iterazione della mappa non è specificato, ma in pratica è
casuale e varia da un'esecuzione all'altra. Questo design è intenzionale, in quanto impedisce ai
programmi di affidarsi a un ordine particolare quando non è garantito.
Passiamo al pacchetto bufio, che aiuta a rendere l'input e l'output efficienti e convenienti. Una delle sue
caratteristiche più utili è un tipo chiamato Scanner, che legge l'input e lo suddivide in righe o parole;
spesso è il modo più semplice per elaborare input che si presentano naturalmente in righe.
Il programma utilizza una breve dichiarazione di variabile per creare una nuova variabile di input che si
riferisce a una variabile di tipo
bufio.Scanner:

input := bufio.NewScanner(os.Stdin)

www.it-ebooks.info
10 CAPITOLO 1. TUTORIAL

Lo scanner legge dallo standard input del programma. Ogni chiamata a input.Scan() legge la riga
successiva e rimuove il carattere newline dalla fine; il risultato può essere recuperato chiamando
input.Text(). La funzione Scan restituisce true se c'è una riga e false se non c'è più input.

La funzione fmt.Printf, come printf in C e in altri linguaggi, produce un output formattato da un


elenco di espressioni. Il suo primo argomento è una stringa di formato che specifica come devono essere
formattati gli argomenti successivi. Il formato di ogni argomento è determinato da un carattere di
conversione, una lettera seguita da un segno di percentuale. Ad esempio, %d formatta un operando
intero utilizzando la notazione decimale, mentre %s si espande al valore di un operando stringa.

Printf ha più di una dozzina di conversioni di questo tipo, che i programmatori Go chiamano verbi.
Questa tabella non è certo una specifica completa, ma illustra molte delle funzionalità disponibili:
%d intero decimale
%x, %o, %b intero in esadecimale, ottale, binario
%f, %g, %e numero in virgola mobile: 3,141593 3,141592653589793 3,141593e+00
%t booleano: vero o falso
%c runa (punto di codice Unicode)
%s stringa
%q stringa quotata "abc" o runa 'c'
%v qualsiasi valore in formato naturale
%T tipo di qualsiasi valore
%% segno percentuale letterale (senza operando)

La stringa di formato in dup1 contiene anche una tabulazione \t e una newline \n. I letterali di stringa
possono contenere tali sequenze di escape per rappresentare caratteri altrimenti invisibili. Printf non
scrive una newline per impostazione predefinita. Per convenzione, le funzioni di formattazione il cui
nome termina in f, come log.Printf e fmt.Errorf, utilizzano le regole di formattazione di
fmt.Printf, mentre quelle il cui nome termina in ln seguono Println, formattando i loro argomenti
come se fossero %v, seguiti da una newline.

Molti programmi leggono dal loro input standard, come sopra, o da una sequenza di file nominati. La
prossima versione di dup può leggere dall'input standard o gestire un elenco di nomi di file, utilizzando
os.Open per aprire ciascuno di essi:

gopl.io/ch1/dup2
// Dup2 stampa il conteggio e il testo delle righe che compaiono più di una volta
// nell'input. Legge da stdin o da un elenco di file nominati. pacchetto main

importare (
"bufio"
"fmt"
"os"
)

www.it-ebooks.info
SEZIONE 1.3. RICERCA DI LINEE DUPLICATE 11

func main() {
counts := make(map[string]int) files :=
os.Args[1:]
if len(files) == 0 { countLines(os.Stdin,
counts)
} else {
for _, arg := range file { f, err
:= os.Open(arg) if err : =
nil {
fmt.Fprintf(os.Stderr, "dup2: %v\n", err) continua
}
countLines(f, counts) f.Close()
}
}
per riga, n := intervallo di conteggi
{ se n > 1 {
fmt.Printf("%d\t%s\n", n, riga)
}
}
}
func countLines(f *os.File, counts map[string]int) { input :=
bufio.NewScanner(f)
for input.Scan() { conta[input.Text()]++
}
// NOTA: ignorare i potenziali errori di input.Err()
}

La funzione os.Open restituisce due valori. Il primo è un file aperto (*os.File) che viene utilizzato
nelle letture successive da Scanner.
Il secondo risultato di os.Open è un valore del tipo di errore incorporato. Se err è uguale al valore
speciale incorporato nil, il file è stato aperto con successo. Il file viene letto e quando si raggiunge la
fine dell'input, Close chiude il file e rilascia tutte le risorse. Se invece err non è nil, qualcosa è andato
storto. In questo caso, il valore di errore descrive il problema. La nostra semplice gestione degli errori
stampa un messaggio sul flusso di errore standard utilizzando Fprintf e il verbo %v, che visualizza
un valore di qualsiasi tipo in un formato predefinito, e dup prosegue con il file successivo;
l'istruzione continue passa all'iterazione successiva del ciclo for.
Nell'interesse di mantenere gli esempi di codice a dimensioni ragionevoli, i nostri primi esempi sono
intenzionalmente un po' cavillosi sulla gestione degli errori. È chiaro che dobbiamo verificare la
presenza di un errore da os.Open; tuttavia, ignoriamo la possibilità, meno probabile, che si verifichi
un errore durante la lettura del file con input.Scan. Noteremo i punti in cui abbiamo saltato il
controllo degli errori e approfondiremo i dettagli della gestione degli errori nella Sezione 5.4.
Si noti che la chiamata a countLines precede la sua dichiarazione. Le funzioni e le altre entità a livello di
pacchetto possono essere dichiarate in qualsiasi ordine.

www.it-ebooks.info
12 CAPITOLO 1. TUTORIAL

Una mappa è un riferimento alla struttura di dati creata da make. Quando una mappa viene passata
a una funzione, questa riceve una copia del riferimento, quindi qualsiasi modifica apportata dalla
funzione chiamata alla struttura di dati sottostante sarà visibile anche attraverso il riferimento alla
mappa del chiamante. Nel nostro esempio, i valori inseriti nella mappa counts da countLines sono
visti da main.

Le versioni di dup di cui sopra operano in modalità "streaming", in cui l'input viene letto e suddiviso in
righe a seconda delle necessità, quindi in linea di principio questi programmi possono gestire una
quantità arbitraria di input. Un approccio alternativo consiste nel leggere l'intero input in memoria in
un'unica soluzione, suddividerlo in righe in una sola volta e quindi elaborare le righe. La versione
seguente, dup3, opera in questo modo. Introduce la funzione ReadFile (dal pacchetto io/ioutil), che
legge l'intero contenuto di un file con nome, e strings.Split, che divide una stringa in una fetta di
sottostringhe. (Split è l'opposto di strings.Join, che abbiamo visto in precedenza).

Abbiamo semplificato in qualche modo dup3. Innanzitutto, legge solo i file denominati, non lo standard
input, poiché ReadFile richiede un argomento relativo al nome del file. In secondo luogo, abbiamo
spostato il conteggio delle righe in main, poiché ora è necessario in un solo punto.

gopl.io/ch1/dup3
pacchetto principale

importare (
"fmt"
"io/ioutil"
"os"
"strings"
)

func main() {
conta := make(map[string]int)
for _, filename := range os.Args[1:] { data, err
:= ioutil.ReadFile(filename) if err := nil {
fmt.Fprintf(os.Stderr, "dup3: %v\n", err) continua
}
per _, riga := intervallo strings.Split(string(data), "\n") { conteggi[riga]++
}
}
per riga, n := intervallo di conteggi
{ se n > 1 {
fmt.Printf("%d\t%s\n", n, riga)
}
}
}

ReadFile restituisce una fetta di byte che deve essere convertita in una stringa per poter essere divisa da
stringhe.Split. Parleremo a lungo delle stringhe e dei byte slices nella Sezione 3.5.4.

www.it-ebooks.info
SEZIONE 1.4. GIF ANIMATE 13

Sotto le coperte, bufio.Scanner, ioutil.ReadFile e ioutil.WriteFile utilizzano i metodi Read e


Write di *os.File, ma è raro che la maggior parte dei programmatori abbia bisogno di accedere
direttamente a queste routine di livello inferiore. Le funzioni di livello superiore, come quelle di bufio
e io/ioutil, sono più facili da usare.
Esercizio 1.4: Modificare dup2 per stampare i nomi di tutti i file in cui si trova ogni riga duplicata.

1.4. GIF animate

Il prossimo programma mostra l'uso di base dei pacchetti immagine standard di Go, che useremo per
creare una sequenza di immagini bitmap e poi codificare la sequenza come animazione GIF. Le
immagini, chiamate figure di Lissajous, erano un effetto visivo fondamentale nei film di fantascienza
degli anni Sessanta. Si tratta di curve parametriche prodotte da oscillazioni armoniche in due
dimensioni, come due onde sinusoidali alimentate agli ingressi x e y di un oscilloscopio. La Figura 1.1
mostra alcuni esempi.

Figura 1.1. Quattro figure di Lissajous.

In questo codice sono presenti diversi nuovi costrutti, tra cui le dichiarazioni di const, i tipi struct e i
letterali compositi. A differenza della maggior parte dei nostri esempi, questo coinvolge anche le com-
ponenti in virgola mobile. Discuteremo questi argomenti solo brevemente, rimandando la maggior parte
dei dettagli a capitoli successivi, poiché l'obiettivo principale in questo momento è dare un'idea
dell'aspetto di Go e del tipo di cose che si possono fare facilmente con il linguaggio e le sue librerie.
gopl.io/ch1/lissajous
// Lissajous genera animazioni GIF di figure Lissajous casuali. pacchetto main

importare (
"image"
"image/color"
"image/gif" "io"
"math"
"math/rand" "os"
)

www.it-ebooks.info
14 CAPITOLO 1. TUTORIAL

var palette = []color.Color{color.White, color.Black}


const (
whiteIndex = 0 // primo colore della tavolozza
blackIndex = 1 // colore successivo della
tavolozza
)
func main() {
lissajous(os.Stdout)
}
func lissajous(out io.Writer) { const (
cicli = 5 // numero di giri completi dell'oscillatore x res = 0,001 //
risoluzione angolare
dimensione = 100 // la tela dell'immagine copre [-
size..+size] nframes = 64 // numero di fotogrammi di
animazione
ritardo =8 // ritardo tra i fotogrammi in unità di 10 ms
)
freq := rand.Float64() * 3.0 // frequenza relativa dell'oscillatore y anim :=
gif.GIF{LoopCount: nframes}
phase := 0.0 // differenza di fase for i
:= 0; i < nframes; i++ {
rect := image.Rect(0, 0, 2*size+1, 2*size+1) img :=
image.NewPaletted(rect, palette)
per t := 0.0; t < cicli*2*math.Pi; t += res { x :=
math.Sin(t)
y := math.Sin(t*freq + fase) img.SetColorIndex(size+int(x*size+0.5),
size+int(y*size+0.5),
blackIndex)
}
fase += 0,1
anim.Delay = append(anim.Delay, delay)
anim.Image = append(anim.Image, img)
}
gif.EncodeAll(out, &anim) // NOTA: ignorare gli errori di codifica
}

Dopo aver importato un pacchetto il cui percorso ha più componenti, come image/color, ci si riferisce al
pacchetto con un nome che deriva dall'ultimo componente. Così la variabile color.White
appartiene al pacchetto image/color e gif.GIF appartiene a image/gif.
Una dichiarazione const (§3.6) dà un nome alle costanti, cioè ai valori che vengono fissati a tempo di
compilazione, come i parametri numerici per cicli, frame e ritardo. Come le dichiarazioni var, le
dichiarazioni const possono apparire a livello di pacchetto (in modo che i nomi siano visibili in tutto il
pacchetto) o all'interno di una funzione (in modo che i nomi siano visibili solo all'interno di quella
funzione). Il valore di una costante deve essere un numero, una stringa o un booleano.
Le espressioni []color.Color{...} e gif.GIF{...} sono letterali compositi (§4.2, §4.4.1), una
notazione compatta per istanziare qualsiasi tipo composito di Go da una sequenza di valori di elementi.
In questo caso, il primo è una slice e il secondo è una struct.

www.it-ebooks.info
SEZIONE 1.5. RILEVAMENTO DI UN 15
URL

Il tipo gif.GIF è un tipo struct (§4.4). Una struct è un gruppo di valori chiamati campi, spesso di tipo
diverso, che vengono raccolti in un unico oggetto che può essere trattato come un'unità. La variabile
anim è una struct di tipo gif.GIF. Il letterale struct crea un valore struct il cui campo Loop Count è
impostato su nframes; tutti gli altri campi hanno il valore zero per il loro tipo. Si può accedere ai singoli
campi di una struct usando la notazione a punti, come nelle due assegnazioni finali che aggiornano
esplicitamente i campi Delay e Image di anim.
La funzione lissajous ha due cicli annidati. Il ciclo esterno viene eseguito per 64 iterazioni, ciascuna
delle quali produce un singolo fotogramma dell'animazione. Crea una nuova immagine 201×201 con
una tavolozza di due colori, bianco e nero. Tutti i pixel sono inizialmente impostati sul valore zero della
tavolozza (il colore numero zero della tavolozza), che noi impostiamo sul bianco. Ogni passaggio
attraverso il ciclo interno genera una nuova immagine impostando alcuni pixel sul nero. Il risultato viene
aggiunto, utilizzando la funzione append incorporata (§4.2.1), a un elenco di fotogrammi in anim,
insieme a un ritardo specificato di 80 ms. Infine, la sequenza di fotogrammi e ritardi viene codificata in
formato GIF e scritta nel flusso di uscita out. Il tipo di out è io.Writer, che ci permette di scrivere su
un'ampia gamma di possibili d e s t i n a z i o n i , come vedremo tra poco.
Il ciclo interno gestisce i due oscillatori. L'oscillatore x è semplicemente la funzione sinusoidale.
Anche l'oscillatore y è una sinusoide, ma la sua frequenza rispetto all'oscillatore x è un numero
casuale compreso tra 0 e 3 e la sua fase rispetto all'oscillatore x è inizialmente nulla, ma aumenta a
ogni fotogramma dell'animazione. Il ciclo viene eseguito finché l'oscillatore x non ha completato
cinque cicli completi. A ogni passo, chiama SetColorIndex per colorare di nero il pixel
corrispondente a (x, y), che si trova nella posizione 1 della tavolozza.
La funzione principale richiama la funzione lissajous, indicandole di scrivere sullo standard output, per
cui questo comando produce una GIF animata con fotogrammi simili a quelli della Figura 1.1:
$ go build gopl.io/ch1/lissajous
$ ./lissajous >out.gif

Esercizio 1.5: Cambiare la tavolozza dei colori del programma Lissajous in verde su nero, per
una maggiore autenticità. Per creare il colore web #RRGGBB, utilizzate color.RGBA{0xRR, 0xGG,
0xBB, 0xff}, dove ogni coppia di cifre esadecimali rappresenta l'intensità della componente rossa, verde
o blu del pixel.
Esercizio 1.6: Modificate il programma Lissajous per produrre immagini a più colori aggiungendo altri
valori alla tavolozza e visualizzandoli cambiando il terzo argomento di Set- ColorIndex in un
modo interessante.

1.5. Recuperare un URL

Per molte applicazioni, l'accesso alle informazioni da Internet è importante quanto l'accesso al file
system locale. Go fornisce una raccolta di pacchetti, raggruppati sotto la voce net, che facilitano l'invio e
la ricezione di informazioni attraverso Internet, le connessioni di rete di basso livello e la creazione di
server, per i quali le funzioni di concurrency di Go (introdotte nel Capitolo 8) sono particolarmente utili.

www.it-ebooks.info
16 CAPITOLO 1. TUTORIAL

Per illustrare il minimo necessario per recuperare informazioni tramite HTTP, ecco un semplice
programma chiamato fetch che recupera il contenuto di ogni URL specificato e lo stampa come testo
non inter- pretato; è ispirato alla preziosa utility curl. Ovviamente si può fare di più con questi dati, ma
questo mostra l'idea di base. Nel corso del libro useremo spesso questo programma.

gopl.io/ch1/fetch
// Fetch stampa il contenuto trovato in un URL.
pacchetto main

importare (
"fmt"
"io/ioutil"
"net/http" "os"
)

func main() {
for _, url := range os.Args[1:] { resp,
err := http.Get(url)
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
os.Exit(1)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: lettura di %s: %v\n", url, err) os.Exit(1)
}
fmt.Printf("%s", b)
}
}

Questo programma introduce le funzioni di due pacchetti, net/http e io/ioutil. La funzione http.Get
effettua una richiesta HTTP e, se non ci sono errori, restituisce il risultato nella struct response resp.
Il campo Body di resp contiene la risposta del server come flusso leggibile. Successivamente,
ioutil.ReadAll legge l'intera risposta; il risultato viene memorizzato in b. Il flusso Body viene chiuso
per evitare perdite di risorse e Printf scrive la risposta sullo standard output.

$ go build gopl.io/ch1/fetch
$ ./fetch http://gopl.io
<html>
<head>
<titolo>Il linguaggio di programmazione Go</titolo>
...

Se la richiesta HTTP fallisce, fetch segnala invece il fallimento:

www.it-ebooks.info
SEZIONE 1.6. RECUPERO SIMULTANEO DI URL 17

$ ./fetch http://bad.gopl.io
fetch: Get http://bad.gopl.io: dial tcp: lookup bad.gopl.io: no such host

In entrambi i casi di errore, os.Exit(1) provoca l'uscita del processo con un codice di stato pari a 1.
Esercizio 1.7: La chiamata di funzione io.Copy(dst, src) legge da src e scrive su dst. Utilizzatela al
posto di ioutil.ReadAll per copiare il corpo della risposta su os.Stdout senza richiedere un buffer
abbastanza grande da contenere l'intero flusso. Assicuratevi di controllare il risultato dell'errore di io.Copy.
Esercizio 1.8: Modificare fetch per aggiungere il prefisso http:// a ogni URL argomento, se manca.
Si potrebbe usare strings.HasPrefix.
Esercizio 1.9: Modificare fetch per stampare anche il codice di stato HTTP, che si trova in resp.Status.

1.6. Recuperare URL in modo simultaneo

Uno degli aspetti più interessanti e nuovi di Go è il suo supporto per la programmazione concorrente. Si
tratta di un argomento molto vasto, a cui sono dedicati il Capitolo 8 e il Capitolo 9, quindi per ora vi
daremo solo un assaggio dei principali meccanismi di concomitanza di Go, le goroutine e i canali.
Il programma successivo, fetchall, esegue lo stesso fetch del contenuto di un URL dell'esempio
precedente, ma esegue il fetch di molti URL, tutti in contemporanea, in modo che il processo non
richieda più tempo del fetch più lungo anziché della somma di tutti i tempi di fetch. Questa versione di
fetchall scarta le risposte, ma riporta la dimensione e il tempo trascorso per ciascuna di esse:
gopl.io/ch1/fetchall
// Fetchall recupera gli URL in parallelo e ne riporta i tempi e le dimensioni. pacchetto
main

importare (
"fmt"
"io" "io/ioutil"
"net/http"
"os"
"tempo"
)

func main() {
start := time.Now()
ch := make(stringa chan)
per _, url := range os.Args[1:] {
go fetch(url, ch) // avvia una goroutine
}
per l'intervallo os.Args[1:] {
fmt.Println(<-ch) // riceve dal canale ch
}
fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())
}

www.it-ebooks.info
18 CAPITOLO 1. TUTORIAL

func fetch(url string, ch chan<- string) { start :=


time.Now()
resp, err := http.Get(url) if
err != nil {
ch <- fmt.Sprint(err) // invia al canale ch return
}

nbyte, err := io.Copy(ioutil.Discard, resp.Body)


resp.Body.Close() // non perdere risorse
if err != nil {
ch <- fmt.Sprintf("durante la lettura di %s: %v", url, err)
return
}
secs := time.Since(start).Seconds()
ch <- fmt.Sprintf("%.2fs %7d %s", secs, nbytes, url)
}

Ecco un esempio:
$ go build gopl.io/ch1/fetchall
$ ./fetchall https://golang.org http://gopl.io https://godoc.org 0.14s
6852 https://godoc.org
0.16s 7261 https://golang.org
0.48s 2475 http://gopl.io
0,48s trascorsi

Una goroutine è un'esecuzione di funzione concorrente. Un canale è un meccanismo di comunicazione


che consente a una goroutine di passare valori di un tipo specifico a un'altra goroutine. La funzione main
viene eseguita in una goroutine e l'istruzione go crea altre goroutine.
La funzione main crea un canale di stringhe utilizzando make. Per ogni argomento della riga di comando,
l'istruzione go nel primo ciclo di range avvia una nuova goroutine che chiama fetch in modo asincrono
per recuperare l'URL usando http.Get. La funzione io.Copy legge il corpo della risposta e lo scarta
scrivendo nel flusso di output ioutil.Discard. Copy restituisce il conteggio dei byte e gli eventuali
errori. All'arrivo di ogni risultato, fetch invia una riga di riepilogo sul canale ch. Il secondo ciclo di range
in main riceve e stampa queste righe.
Quando una goroutine tenta un invio o una ricezione su un canale, si blocca finché un'altra goroutine
non tenta la corrispondente operazione di ricezione o invio, a quel punto il valore viene trasferito ed
entrambe le goroutine procedono. In questo esempio, ogni fetch invia un valore (ch <- espressione) sul
canale ch e main li riceve tutti (<-ch). Il fatto che main esegua tutte le operazioni di stampa assicura che
l'output di ogni goroutine venga elaborato come un'unità, senza il pericolo di interleaving se due
goroutine terminano nello stesso momento.
Esercizio 1.10: Trovate un sito web che produce una grande quantità di dati. Esaminate la cache
eseguendo fetchall due volte di seguito per vedere se il tempo riportato cambia di molto. Si ottiene ogni
volta lo stesso contenuto? Modificate fetchall per stampare il suo output su un file in modo da poterlo
esaminare.

www.it-ebooks.info
SEZIONE 1.7. UN SERVER WEB 19

Esercizio 1.11: Provate fetchall con elenchi di argomenti più lunghi, come ad esempio i campioni dei
primi milioni di siti web disponibili su alexa.com. Come si comporta il programma se un sito web
non risponde? (La Sezione 8.9 descrive i meccanismi per far fronte a questi casi).

1.7. Un server web

Le librerie di Go consentono di scrivere facilmente un server web che risponde a richieste del cliente
come quelle effettuate da fetch. In questa sezione, mostreremo un server minimale che restituisce il
componente percorso dell'URL utilizzato per accedere al server. Cioè, se la richiesta è per
http://local- host:8000/hello, la risposta sarà URL.Path = "/hello".

gopl.io/ch1/server1
// Server1 è un server "echo" minimale.
pacchetto main

importare (
"fmt"
"log"
"net/http"
)

func main() {
http.HandleFunc("/", handler) // ogni richiesta chiama l'handler
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// handler fa eco alla componente Path dell'URL della richiesta r. func


handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

Il programma è lungo solo una manciata di righe, perché le funzioni di libreria svolgono la maggior
parte del lavoro. La funzione principale collega una funzione di gestione agli URL in arrivo che iniziano
con /, ovvero tutti gli URL, e avvia un server in ascolto per le richieste in arrivo sulla porta 8000. Una
richiesta è rappresentata come una struct di tipo http.Request, che contiene una serie di campi correlati,
uno dei quali è l'URL della richiesta in arrivo. Quando arriva una richiesta, viene data alla funzione
handler, che estrae il componente percorso (/hello) dall'URL della richiesta e lo invia come
risposta, usando fmt.Fprintf. I server Web saranno spiegati in dettaglio nella Sezione 7.7.
Avviamo il server in background. Su Mac OS X o Linux, aggiungete un ampersand (&) al comando; su
Microsoft Windows, dovrete eseguire il comando senza l'ampersand in una finestra di comando
separata.
$ go run src/gopl.io/ch1/server1/main.go &

Possiamo quindi effettuare le richieste dei client dalla riga di comando:

www.it-ebooks.info
20 CAPITOLO 1. TUTORIAL

$ go build gopl.io/ch1/fetch
$ ./fetch http://localhost:8000
URL.Path = "/"
$ ./fetch http://localhost:8000/help URL.Path =
"/help"

In alternativa, si può accedere al server da un browser web, come mostrato nella Figura 1.2.

Figura 1.2. Una risposta dal server echo.

È facile aggiungere funzioni al server. Un'aggiunta utile è un URL specifico che restituisce uno stato di
qualche tipo. Ad esempio, questa versione fa lo stesso eco, ma conta anche il numero di richieste; una
richiesta all'URL /count restituisce il conteggio fino a quel momento, escludendo le richieste di /count
stesse:
gopl.io/ch1/server2
// Server2 è un server "echo" e contatore minimale. pacchetto
main

importare (
"fmt"
"log"
"net/http"
"sync"
)

var mu sync.Mutex
var count int

func main() {
http.HandleFunc("/", handler) http.HandleFunc("/count",
counter) log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// handler fa eco al componente Path dell'URL richiesto. func handler(w


http.ResponseWriter, r *http.Request) {
mu.Lock()
count++
mu.Unlock()
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

www.it-ebooks.info
SEZIONE 1.7. UN SERVER WEB 21

// Il contatore indica il numero di chiamate effettuate f i n o r a .


func counter(w http.ResponseWriter, r *http.Request) { mu.Lock()
fmt.Fprintf(w, "Count %d\n", count) mu.Unlock()
}

Il server ha due gestori e l'URL di richiesta determina quale viene chiamato: una richiesta per /count
invoca counter e tutte le altre invocano il gestore. Un modello di gestore che termina con una barra
corrisponde a qualsiasi URL che abbia il modello come prefisso. Dietro le quinte, il server esegue il
gestore per ogni richiesta in arrivo in una goroutine separata, in modo da poter servire più richieste
contemporaneamente. Tuttavia, se due richieste contemporanee tentano di aggiornare il conteggio,
questo potrebbe non essere incrementato in modo coerente; il programma presenterebbe un grave bug
chiamato race condition (§9.1). Per evitare questo problema, dobbiamo assicurarci che al massimo una
goroutine acceda alla variabile alla volta, il che è lo scopo delle chiamate mu.Lock() e mu.Unlock() che
intervallano ogni accesso a count. Analizzeremo più da vicino la concorrenza con le variabili condivise
nel Capitolo 9.
Come esempio più ricco, la funzione handler può riportare le intestazioni e i dati del modulo che riceve,
rendendo il server utile per l'ispezione e il debug delle richieste:
gopl.io/ch1/server3
// Il gestore fa da eco alla richiesta HTTP.
func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w,
"%s %s %s\n", r.Method, r.URL, r.Proto) per k, v := range r.Header
{
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
fmt.Fprintf(w, "Host = %q\n", r.Host) fmt.Fprintf(w,
"RemoteAddr = %q\n", r.RemoteAddr) if err :=
r.ParseForm(); err != nil {
log.Print(err)
}
per k, v := intervallo r.Form {
fmt.Fprintf(w, "Form[%q] = %q\n", k, v)
}
}

Utilizza i campi della struttura http.Request per produrre un output come questo:
GET /?q=query HTTP/1.1
Header["Accept-Encoding"] = ["gzip, deflate, sdch"]
Header["Accept-Language"] = ["en-US,en;q=0.8"]
Header["Connection"] = ["keep-alive"]
Header["Accept"] = ["text/html,application/xhtml+xml,application/xml;..."] Header["User-
Agent"] = ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5)..."] Host = "localhost:8000"
RemoteAddr = "127.0.0.1:59911"
Form["q"] = ["query"].

www.it-ebooks.info
22 CAPITOLO 1. TUTORIAL

Notate come la chiamata a ParseForm sia annidata all'interno di una dichiarazione if. Go permette
che un semplice stato, come la dichiarazione di una variabile locale, preceda la condizione if, il che
è particolarmente utile per la gestione degli errori, come in questo esempio. Avremmo potuto
scriverlo come
err := r.ParseForm() if
err : = nil {
log.Print(err)
}

ma combinare le istruzioni è più breve e riduce l'ambito della variabile err, il che è una buona
pratica. Definiremo l'ambito nella Sezione 2.7.

In questi programmi, abbiamo visto tre tipi diversi di flussi di uscita. Il programma fetch copiava i dati
della risposta HTTP in os.Stdout, un file, come faceva il programma lissajous. Il programma
fetchall getta via la risposta (contando la sua lunghezza) copiandola nel banale sink ioutil.Discard. Il
server web di cui sopra ha usato fmt.Fprintf per scrivere a un http.ResponseWriter che
rappresenta il browser web.

Sebbene questi tre tipi differiscano nei dettagli di ciò che fanno, soddisfano tutti un'interfaccia comune,
che consente di utilizzare uno qualsiasi di essi ovunque sia necessario un flusso di output. Questa
interfaccia, chiamata io.Writer, è discussa nella Sezione 7.1.

Il meccanismo di interfaccia di Go è l'argomento del Capitolo 7, ma per dare un'idea di ciò che è in
grado di fare, vediamo come è facile combinare il server web con la funzione lissajous, in modo che le
GIF ani- mate non vengano scritte sullo standard output, ma sul client HTTP. Basta aggiungere queste
righe al server web:
handler := func(w http.ResponseWriter, r *http.Request) { lissajous(w)
}
http.HandleFunc("/", handler)

o equivalentemente:
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { lissajous(w)
})

Il secondo argomento della chiamata di funzione HandleFunc, immediatamente sopra, è un letterale di


funzione, cioè una funzione anonima definita al momento dell'uso. Lo spiegheremo meglio nella Sezione
5.6.

Una volta apportata questa modifica, visitate http://localhost:8000 nel vostro browser. Ogni volta che
caricherete la pagina, vedrete una nuova animazione, come quella della Figura 1.3.

Esercizio 1.12: Modificare il server Lissajous per leggere i valori dei parametri dall'URL. Ad esempio, si
potrebbe fare in modo che un URL come http://localhost:8000/?cycles=20 imposti il numero di
cicli a 20 invece dei 5 predefiniti. Utilizzare la funzione strconv.Atoi per convertire il parametro stringa
in un numero intero. La documentazione è disponibile con go doc strconv.Atoi.

www.it-ebooks.info
SEZIONE 1.8. ESTREMITÀ 23
LIBERE

Figura 1.3. Figure di Lissajous animate in un browser.

1.8. Fine della storia

Go è molto di più di quanto abbiamo trattato in questa rapida introduzione. Ecco alcuni argomenti che
abbiamo appena accennato o omesso del tutto, con una trattazione sufficiente a renderli familiari
quando fanno una breve apparizione prima della trattazione completa.

Flusso di controllo: Abbiamo trattato le due istruzioni fondamentali del flusso di controllo, if e for, ma
non l'istruzione switch, che è un ramo a più vie. Ecco un piccolo esempio:

switch coinflip() {
caso "testa":
heads++
caso "tails":
code++
default:
fmt.Println("atterrato sul bordo!")
}

Il risultato del lancio della moneta viene confrontato con il valore di ciascun caso. I casi vengono
valutati dall'alto verso il basso, quindi viene eseguito il primo caso corrispondente. Il caso opzionale
predefinito corrisponde se nessuno degli altri casi lo fa; può essere posizionato ovunque. I casi non
passano da uno all'altro come nei linguaggi simili al C (anche se esiste un'istruzione fallthrough, usata
raramente, che annulla questo comportamento).

Uno switch non ha bisogno di un operando; può semplicemente elencare i casi, ognuno dei quali è
un'espressione booleana:

www.it-ebooks.info
24 CAPITOLO 1. TUTORIAL

func Signum(x int) int { switch {


caso x > 0:
ritorno +1
predefinito:
restituire 0
caso x < 0:
restituire -1
}
}

Questa forma è chiamata switch senza tag; è equivalente a switch true.

Come gli enunciati for e if, uno switch può includere una semplice dichiarazione opzionale, una
breve dichiarazione di variabile, un'istruzione di incremento o assegnazione o una chiamata di
funzione, che può essere usata per impostare un valore prima che venga testato.

Le istruzioni break e continue modificano il flusso di controllo. Un'interruzione fa sì che il controllo


riprenda dall'istruzione successiva all'istruzione for, switch o select più interna (che vedremo più
avanti) e, come abbiamo visto nella Sezione 1.3, un continue fa sì che il ciclo for più interno inizi la
sua iterazione successiva. Le istruzioni possono essere etichettate in modo che break e continue
possano riferirsi ad esse, ad esempio per uscire da diversi cicli annidati in una sola volta o per avviare
l'iterazione successiva del ciclo più esterno. Esiste anche un'istruzione goto, anche se è destinata al
codice generato dalla macchina e non all'uso regolare da parte dei programmatori.

Tipi con nome: Una dichiarazione di tipo consente di dare un nome a un tipo esistente. Poiché i tipi
struct sono spesso lunghi, sono quasi sempre denominati. Un esempio familiare è la definizione di un
tipo Point per un sistema grafico 2-D:
tipo Punto struct { X, Y
int
}
var p Punto

Le dichiarazioni di tipo e i tipi denominati sono trattati nel Capitolo 2.

Puntatori: Go fornisce puntatori, cioè valori che contengono l'indirizzo di una variabile. In alcuni
linguaggi, in particolare il C, i puntatori sono relativamente liberi. In altri linguaggi, i puntatori sono
mascherati da "riferimenti" e non si può fare molto con loro, se non passarli. Go si colloca a metà strada.
I puntatori sono esplicitamente visibili. L'operatore & fornisce l'indirizzo di una variabile e l'operatore *
recupera la variabile a cui si riferisce il puntatore, ma non esiste l'aritmetica dei puntatori. Spiegheremo i
puntatori nella Sezione 2.3.2.

Metodi e interfacce: Un metodo è una funzione associata a un tipo denominato; Go è insolito in quanto i
metodi possono essere associati a quasi tutti i tipi denominati. I metodi sono trattati nel Capitolo 6. Le
interfacce sono tipi astratti che ci permettono di trattare diversi tipi concreti allo stesso modo, in base ai
metodi che hanno, non a come sono rappresentati o implementati. Le interfacce sono oggetto del
Capitolo 7.

www.it-ebooks.info
SEZIONE 1.8. ESTREMITÀ 25
LIBERE

Pacchetti: Go è dotato di un'ampia libreria standard di pacchetti utili e la comunità di Go ne ha creati e


condivisi molti altri. Spesso la programmazione si basa più sull'uso dei pacchetti esistenti che sulla
scrittura di codice originale. Nel corso del libro, indicheremo un paio di dozzine dei pacchetti standard
più importanti, ma ce ne sono molti altri che non abbiamo spazio per menzionare e non possiamo
fornire nulla di simile a un riferimento completo per ogni pacchetto.
Prima di intraprendere un nuovo programma, è una buona idea vedere se esistono già dei pacchetti che
potrebbero aiutarvi a svolgere il vostro lavoro più facilmente. L'indice dei pacchetti della libreria
standard si trova all'indirizzo https://golang.org/pkg, mentre quello dei pacchetti forniti dalla
comunità all'indirizzo https://godoc.org. Lo strumento go doc rende questi documenti
facilmente accessibili dalla riga di comando:
$ go doc http.ListenAndServe pacchetto
http // importazione "net/http"

func ListenAndServe(addr string, handler Handler) error ListenAndServe si mette in

ascolto sull'indirizzo di rete TCP addr e poi


serve con un gestore per gestire le richieste sulle connessioni in arrivo.
...

Commenti: Abbiamo già parlato dei commenti di documentazione all'inizio di un programma o di un


pacchetto. È anche buona norma scrivere un commento prima della dichiarazione di ogni funzione per
specificarne il comportamento. Queste convenzioni sono importanti, perché vengono utilizzate da
strumenti come go doc e godoc per individuare e visualizzare la documentazione (§10.7.4).
Per i commenti che si estendono su più righe o che appaiono all'interno di un'espressione o di
un'istruzione, esiste anche la notazione /* ... */, già nota in altri linguaggi. Tali commenti sono
talvolta utilizzati all'inizio di un file per un grande blocco di testo esplicativo, per evitare di inserire
un // su ogni riga. All'interno di un commento, // e /* non hanno un significato particolare, quindi
i commenti non si annidano.

www.it-ebooks.info
Questa pagina è stata lasciata intenzionalmente in bianco

www.it-ebooks.info
2
Struttura del
programma

In Go, come in qualsiasi altro linguaggio di programmazione, si costruiscono grandi programmi a


partire da un piccolo insieme di costrutti di base. Le variabili memorizzano valori. Le espressioni
semplici vengono combinate in espressioni più grandi con operazioni come l'addizione e la sottrazione. I
tipi di base vengono raccolti in aggregati come array e struct. Le espressioni vengono utilizzate in
istruzioni il cui ordine di esecuzione è determinato da istruzioni di flusso di controllo come if e for.
Le istruzioni vengono raggruppate in funzioni per isolarle e riutilizzarle. Le funzioni vengono
raccolte in file sorgente e pacchetti.
Abbiamo visto esempi della maggior parte di questi elementi nel capitolo precedente. In questo capitolo
approfondiremo gli elementi strutturali di base di un programma Go. I programmi di esempio sono
intenzionalmente semplici, in modo da potersi concentrare sul linguaggio senza farsi distrarre da
complicati algoritmi o strutture di dati.

2.1. Nomi

I nomi delle funzioni, delle variabili, delle costanti, dei tipi, delle etichette delle istruzioni e dei pacchetti
di Go seguono una semplice regola: un nome inizia con una lettera (cioè tutto ciò che Unicode considera
una lettera) o un trattino basso e può avere un numero qualsiasi di lettere, cifre e trattini bassi aggiuntivi.
Casi particolari: heapSort e Heapsort sono nomi diversi.
Go ha 25 parole chiave, come if e switch, che possono essere usate solo dove la sintassi lo consente;
non possono essere usate come nomi.
pausa predefinito func interfaccia selezio
nare
caso rinviare andare mappa struttu
ra
chan altro goto pacchetto interrut
tore
costitutivo caduta se gamma tipo

www.it-ebooks.info
continuare per Importazi ritorno var
one
27

www.it-ebooks.info
28 CAPITOLO 2. STRUTTURA DEL
PROGRAMMA

Inoltre, ci sono circa tre dozzine di nomi predeterminati, come int e true, per i con- stanti, i tipi e le
funzioni incorporate:
Costanti: true false iota nil

Tipi: int int8 int16 int32 int64


uint uint8 uint16 uint32 uint64 uintptr float32
float64 complex128 complex64 bool byte rune
string error

Funzioni: make len cap new append copy close delete complex real
imag
panico recuperare
Questi nomi non sono riservati, quindi si possono usare nelle dichiarazioni. Vedremo una manciata di posti
in cui la ridichiarazione di uno di essi ha senso, ma attenzione alla potenziale confusione.
Se un'entità è dichiarata all'interno di una funzione, è locale a quella funzione. Se invece viene dichiarata
al di fuori di una funzione, è visibile in tutti i file del pacchetto a cui appartiene. Il caso della prima
lettera di un nome determina la sua visibilità oltre i confini del pacchetto. Se il nome inizia con una
lettera maiuscola, è esportato, il che significa che è visibile e accessibile al di fuori del proprio pacchetto e
può essere citato da altre parti del programma, come nel caso di Printf nel pacchetto fmt. I nomi dei
pacchetti sono sempre in minuscolo.
Non c'è un limite alla lunghezza dei nomi, ma le convenzioni e lo stile dei programmi Go propendono
per nomi brevi, soprattutto per le variabili locali con ambiti ridotti; è molto più probabile che si
vedano variabili chiamate i piuttosto che theLoopIndex. In generale, più grande è l'ambito di un
nome, più lungo e significativo dovrebbe essere.
Stilisticamente, i programmatori Go usano il ''camel case'' quando formano i nomi combinando le
parole; cioè, le lettere maiuscole interne sono preferite ai trattini bassi interni. Così le librerie standard
hanno funzioni con nomi come QuoteRuneToASCII e parseRequestLine ma mai
quote_rune_to_ASCII o parse_request_line. Le lettere di acronimi e iniziali come ASCII e HTML
sono sempre rese nello stesso caso, quindi una funzione potrebbe chiamarsi html- Escape, HTMLEscape o
escapeHTML, ma non escapeHtml.

2.2. Dichiarazioni

Una dichiarazione nomina un'entità del programma e specifica alcune o tutte le sue proprietà. Esistono
quattro tipi principali di dichiarazioni: var, const, type e func. In questo capitolo parleremo di variabili e
tipi, nel Capitolo 3 di costanti e nel Capitolo 5 di funzioni.
Un programma Go è memorizzato in uno o più file il cui nome termina con .go. Ogni file inizia con una
dichiarazione di pacchetto che dice di quale pacchetto fa parte il file. La dichiarazione del pacchetto è
seguita da eventuali dichiarazioni di importazione e poi da una sequenza di dichiarazioni a livello di
pacchetto d i tipi, variabili, costanti e funzioni, in qualsiasi ordine. Ad esempio, questo programma
dichiara una costante, una funzione e un paio di variabili:

www.it-ebooks.info
SEZIONE 2.2. DICHIARAZIONI 29

gopl.io/ch2/bollente
// Bollire stampa il punto di ebollizione dell'acqua.
pacchetto main

importare "fmt"

const boilingF = 212.0 func

main() {
var f = boilingF
var c = (f - 32) * 5 / 9
fmt.Printf("punto di ebollizione = %g°F o %g°C\n", f, c)
// Uscita:
// punto di ebollizione = 212°F o 100°C
}

La costante boilingF è una dichiarazione a livello di pacchetto (così come main), mentre le variabili f e c
sono locali alla funzione main. Il nome di ogni entità a livello di pacchetto è visibile non solo nel file
sorgente che contiene la sua dichiarazione, ma in tutti i file del pacchetto. Al contrario, le dichiarazioni
locali sono visibili solo all'interno della funzione in cui sono dichiarate e forse solo in una piccola parte
di essa.

Una dichiarazione di funzione ha un nome, un elenco di parametri (le variabili i cui valori sono forniti
dai chiamanti della funzione), un elenco opzionale di risultati e il corpo della funzione, che contiene le
dichiarazioni che definiscono ciò che la funzione fa. L'elenco dei risultati viene omesso se la funzione
non restituisce nulla. L'esecuzione della funzione inizia con la prima istruzione e continua finché non
incontra un'istruzione di ritorno o raggiunge la fine di una funzione che non ha risultati. Il controllo e
gli eventuali risultati vengono restituiti al chiamante.

Abbiamo già visto un discreto numero di funzioni e ce ne saranno molte altre, compresa un'ampia
discussione nel Capitolo 5, quindi questo è solo uno schizzo. La funzione fToC qui sotto incapsula la
logica di conversione della temperatura in modo che sia definita una sola volta ma possa essere utilizzata
da più punti. Qui main la chiama due volte, utilizzando i valori di due diverse costanti locali:
gopl.io/ch2/ftoc
// Ftoc stampa due conversioni da Fahrenheit a Celsius. pacchetto
main

importare "fmt"

func main() {
costanti congelamentoF, ebollizioneF = 32.0, 212.0
fmt.Printf("%g°F = %g°C\n", freezingF, fToC(freezingF)) // "32°F = 0°C" fmt.Printf("%g°F =
%g°C\n", boilingF, fToC(boilingF)) // "212°F = 100°C"
}

func fToC(f float64) float64 {


restituisce (f - 32) * 5 / 9
}

www.it-ebooks.info
30 CAPITOLO 2. STRUTTURA DEL
PROGRAMMA

2.3. Variabili

Una dichiarazione var crea una variabile di un particolare tipo, le assegna un nome e ne imposta il valore
iniziale. Ogni dichiarazione ha la forma generale
var nome tipo = espressione

È possibile omettere il tipo o la parte dell'espressione =, ma non entrambi. Se il tipo viene omesso,
viene determinato dall'espressione dell'inizializzatore. Se l'espressione è omessa, il valore iniziale è il
valore zero del tipo, ovvero 0 per i numeri, false per i booleani, "" per le stringhe e nil per le interfacce e
i tipi di riferimento (slice, pointer, map, channel, function). Il valore zero di un tipo aggregato come un
array o una struct ha il valore zero di tutti i suoi elementi o campi.
Il meccanismo del valore zero assicura che una variabile contenga sempre un valore ben definito del suo
tipo; in Go non esiste una variabile non inizializzata. Questo semplifica il codice e spesso assicura un
comportamento sensato delle condizioni al contorno, senza bisogno di lavoro aggiuntivo. Per esempio,
var s stringa
fmt.Println(s) // ""

stampa una stringa vuota, invece di causare qualche tipo di errore o comportamento imprevedibile. I
programmatori di Go spesso si sforzano di rendere significativo il valore zero di un tipo più complicato,
in modo che le variabili inizino la loro vita in uno stato utile.
È possibile dichiarare e facoltativamente inizializzare un insieme di variabili in un'unica dichiarazione,
con un elenco di espressioni corrispondenti. L'omissione del tipo consente di dichiarare più variabili di
tipo diverso:
var i, j, k int // int, int, int
var b, f, s = true, 2.3, "quattro" // bool, float64, stringa

Gli inizializzatori possono essere valori letterali o espressioni arbitrarie. Le variabili a livello di pacchetto
vengono inizializzate prima dell'inizio del main (§2.6.2), mentre le variabili locali vengono inizializzate
quando le loro dichiarazioni vengono incontrate durante l'esecuzione della funzione.
Un insieme di variabili può essere inizializzato anche chiamando una funzione che restituisce più valori:
var f, err = os.Open(nome) // os.Open restituisce un file e un errore

2.3.1. Dichiarazioni brevi di variabili

All'interno di una funzione, per dichiarare e inizializzare le variabili locali si può utilizzare una forma
alternativa chiamata dichiarazione di variabile breve. Essa assume la forma nome := espressione, e il
tipo di nome è determinato dal tipo di espressione. Ecco tre delle numerose dichiarazioni di variabili
brevi presenti nella funzione lissajous (§1.4):
anim := gif.GIF{LoopCount: nframes} freq :=
rand.Float64() * 3.0
t := 0.0

www.it-ebooks.info
SEZIONE 2.3. VARIABILI 31

Per la loro brevità e flessibilità, le dichiarazioni di variabile breve sono utilizzate per dichiarare e
inizializzare la maggior parte delle variabili locali. Una dichiarazione var tende a essere riservata alle
variabili locali che necessitano di un tipo esplicito diverso da quello dell'espressione inizializzante,
oppure quando alla variabile verrà assegnato un valore in seguito e il suo valore iniziale non è
importante.
i := 100 // un int
var bollente float64 = 100 // un float64

var nomi []stringa var


err errore
var p Punto

Come per le dichiarazioni di var, più variabili possono essere dichiarate e inizializzate nella stessa
dichiarazione di variabile breve,
i, j := 0, 1

ma le dichiarazioni con più espressioni di inizializzazione dovrebbero essere usate solo quando aiutano
la lettura, come nel caso di raggruppamenti brevi e naturali, come la parte di inizializzazione di un ciclo
for.

Si tenga presente che := è una dichiarazione, mentre = è un'assegnazione. Una dichiarazione di più
variabili non deve essere confusa con un'assegnazione di tuple (§2.4.1), in cui a ogni variabile del lato
sinistro viene assegnato il valore corrispondente del lato destro:
i, j = j, i // scambiare i valori di i e j

Come le normali dichiarazioni di var, le dichiarazioni di variabile breve possono essere utilizzate per le
chiamate a funzioni come os.Open che restituiscono due o più valori:
f, err := os.Open(nome) if
err := nil {
restituire err
}
// ...usare f...
f.Close()

Un punto sottile ma importante: una dichiarazione breve di variabile non dichiara necessariamente tutte
le variabili sul suo lato sinistro. Se alcune di esse sono già state dichiarate nello stesso blocco lessicale
(§2.7), la dichiarazione di variabile breve si comporta come un'assegnazione a quelle variabili.
Nel codice sottostante, la prima istruzione dichiara sia in che err. La seconda dichiara out, ma assegna
solo un valore alla variabile err esistente.
in, err := os.Open(infile)
// ...
out, err := os.Create(outfile)

Tuttavia, una dichiarazione di variabile breve deve dichiarare almeno una nuova variabile, quindi questo
codice non verrà compilato:
f, err := os.Open(infile)
// ...
f, err := os.Create(outfile) // errore di compilazione: nessuna nuova variabile

www.it-ebooks.info
32 CAPITOLO 2. STRUTTURA DEL
PROGRAMMA

La soluzione consiste nell'utilizzare un assegnamento ordinario per la seconda istruzione.


Una breve dichiarazione di variabile agisce come un'assegnazione solo per le variabili già dichiarate nello
stesso blocco lessicale; le dichiarazioni in un blocco esterno vengono ignorate. Ne vedremo un esempio
alla fine del capitolo.

2.3.2. Puntatori

Una variabile è una porzione di memoria contenente un valore. Le variabili create dalle dichiarazioni
sono identificate da un nome, come x, ma molte variabili sono identificate solo da espressioni come x[i]
o
x.f. Tutte queste espressioni leggono il valore di una variabile, tranne quando appaiono sul lato sinistro di
un'assegnazione, nel qual caso viene assegnato un nuovo valore alla variabile.
Il valore di un puntatore è l'indirizzo di una variabile. Un puntatore è quindi la posizione in cui viene
memorizzato un valore. Non tutti i valori hanno un indirizzo, ma tutte le variabili ce l'hanno. Con un
puntatore, possiamo leggere o aggiornare il valore di una variabile in modo indiretto, senza utilizzare o
conoscere il nome della variabile, ammesso che abbia un nome.
Se una variabile è dichiarata var x int, l'espressione &x (''indirizzo di x'') produce un puntatore a una
variabile intera, cioè un valore di tipo *int, che si pronuncia ''puntatore a int''. Se questo valore si chiama
p, si dice ''p punta a x'', o equivalentemente ''p contiene l'indirizzo di x''. La variabile a cui punta p si scrive
*p. L'espressione *p produce il valore di quella variabile, un int, ma poiché *p denota una variabile, può
anche apparire sul lato sinistro di un'assegnazione, nel qual caso l'assegnazione aggiorna la variabile.
x := 1
p := &x // p, di tipo *int, punta a x
fmt.Println(*p) // "1"
*p = 2 // equivalente a x = 2
fmt.Println(x) // "2"

Ogni componente di una variabile di tipo aggregato - un campo di una struct o un elemento di un array
- è anch'esso una variabile e quindi ha un indirizzo.
Le variabili sono talvolta descritte come valori indirizzabili. Le espressioni che denotano le variabili sono
le uniche a cui può essere applicato l'operatore &.
Il valore zero per un puntatore di qualsiasi tipo è nil. Il test p != nil è vero se p punta a una
variabile. I puntatori sono confrontabili; due puntatori sono uguali se e solo se puntano alla stessa
variabile o se entrambi sono nulli.
var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) // "vero falso falso"

È perfettamente sicuro che una funzione restituisca l'indirizzo di una variabile locale. Ad esempio, nel
codice seguente, la variabile locale v creata da questa particolare chiamata a f rimarrà in vita anche dopo
il ritorno della chiamata e il puntatore p continuerà a farvi riferimento:

www.it-ebooks.info
SEZIONE 2.3. VARIABILI 33

var p = f()

func f() *int { v


:= 1
restituire &v
}

Ogni chiamata di f restituisce un valore distinto:


fmt.Println(f() == f()) // "falso"

Poiché un puntatore contiene l'indirizzo di una variabile, il passaggio di un argomento puntatore a una
funzione consente alla funzione di aggiornare la variabile passata indirettamente. Ad esempio, questa
funzione incrementa la variabile a cui punta il suo argomento e restituisce il nuovo valore della variabile,
che può essere utilizzato in un'espressione:
func incr(p *int) int {
*p++ // incrementa ciò a cui punta p; non cambia p return *p
}

v := 1
incr(&v) // effetto collaterale: v è ora 2
fmt.Println(incr(&v)) // "3" (e v è 3)

Ogni volta che prendiamo l'indirizzo di una variabile o copiamo un puntatore, creiamo nuovi alias o
modi per identificare la stessa variabile. Ad esempio, *p è un alias di v. L'aliasing dei puntatori è utile
perché ci permette di accedere a una variabile senza usare il suo nome, ma è un'arma a doppio taglio: per
trovare tutte le istruzioni che accedono a una variabile, dobbiamo conoscere tutti i suoi alias. Non sono
solo i puntatori a creare alias; l'aliasing si verifica anche quando si copiano valori di altri tipi di
riferimento, come slices, mappe e canali, e persino strutture, array e interfacce che contengono questi
tipi.
I puntatori sono fondamentali per il pacchetto flag, che utilizza gli argomenti della riga di comando di un
programma per impostare i valori di alcune variabili distribuite nel programma. Per esempio, questa
variante del precedente comando echo accetta due flag opzionali: -n fa sì che echo ometta il
newline di coda che verrebbe normalmente stampato e -s sep fa sì che separi gli argomenti in
uscita con il contenuto della stringa sep invece che con il singolo spazio predefinito. Poiché questa è la
quarta versione, il pacchetto si chiama gopl.io/ch2/echo4.
gopl.io/ch2/echo4
// Echo4 stampa gli argomenti della riga di comando.
pacchetto main

importare (
"bandiera"
"fmt"
"stringhe"
)

var n = flag.Bool("n", false, "omettere la linea di


separazione") var sep = flag.String("s", " ", "separatore")

www.it-ebooks.info
34 CAPITOLO 2. STRUTTURA DEL
PROGRAMMA

func main() {
flag.Parse() fmt.Print(strings.Join(flag.Args(),
*sep)) if !*n {
fmt.Println()
}
}

La funzione flag.Bool crea una nuova variabile flag di tipo bool. Richiede tre argomenti: il nome del flag
("n"), il valore predefinito della variabile (false) e un messaggio che verrà stampato se l'utente fornisce
un argomento non valido, un flag non valido, oppure -h o -help. Analogamente, flag.String
prende un nome, un valore predefinito e un messaggio e crea una variabile stringa. Le variabili
sep e n sono puntatori alle variabili flag, a cui si deve accedere indirettamente come
*sep e *n.
Quando il programma viene eseguito, deve chiamare flag.Parse prima che i flag vengano utilizzati,
per aggiornare le variabili dei flag dai loro valori predefiniti. Gli argomenti non legati ai flag sono
disponibili in flag.Args() come una fetta di stringhe. Se flag.Parse incontra un errore, stampa un
messaggio di utilizzo e chiama os.Exit(2) per terminare il programma.
Eseguiamo alcuni casi di test su echo:
$ go build gopl.io/ch2/echo4
$ ./echo4 a bc def a bc
def
$ ./echo4 -s / a bc def a/bc/def
$ ./echo4 -n a bc def a bc
def$
$ ./echo4 -help Uso
di ./echo4:
-n omette la newline finale
stringa -s
separatore (predefinito " ")

2.3.3. La nuova funzione

Un altro modo per creare una variabile è utilizzare la funzione built-in new. L'espressione new(T) crea
una variabile senza nome di tipo T, la inizializza al valore zero di T e restituisce il suo indirizzo, che è un
valore di tipo *T.
p := new(int) // p, di tipo *int, punta a una variabile int senza nome
fmt.Println(*p) // "0"
*p = 2 // imposta l'int senza nome a 2
fmt.Println(*p) // "2"

Una variabile creata con new non è diversa da una normale variabile locale di cui viene preso l'indirizzo,
tranne per il fatto che non è necessario inventare (e dichiarare) un nome fittizio e che possiamo usare
new(T) in un'espressione. Quindi new è solo una comodità sintattica, non una nozione fondamentale:

www.it-ebooks.info
SEZIONE 2.3. VARIABILI 35

le due funzioni newInt di seguito hanno comportamenti identici.


func newInt() *int { func newInt() *int {
return new(int) var dummy int
} return &dummy
}

Ogni chiamata a new restituisce una variabile distinta con un indirizzo unico:
p := nuovo(int)
q := nuovo(int)
fmt.Println(p == q) // "falso"

C'è un'eccezione a questa regola: due variabili il cui tipo non contiene informazioni ed è quindi di
dimensione zero, come struct{} o [0]int, possono, a seconda dell'implementazione, a v e r e lo stesso
indirizzo.
La nuova funzione è usata relativamente di rado perché le variabili senza nome più comuni sono di tipo
struct, per le quali la sintassi dei letterali struct (§4.4.1) è più flessibile.
Poiché new è una funzione predichiarata, non una parola chiave, è possibile ridefinire il nome per
qualcos'altro all'interno di una funzione, ad esempio:
func delta(old, new int) int { restituisce new - old }

Naturalmente, all'interno di delta, la funzione built-in new non è disponibile.

2.3.4. Durata delle variabili

Il tempo di vita di una variabile è l'intervallo di tempo durante il quale essa esiste durante l'esecuzione del
programma. Il tempo di vita di una variabile a livello di pacchetto è l'intera esecuzione del programma.
Le variabili locali, invece, hanno una vita dinamica: una nuova istanza viene creata ogni volta che viene
eseguita l'istruzione di dichiarazione e la variabile continua a vivere finché non diventa irraggiungibile, a
quel punto la sua memoria può essere riciclata. Anche i parametri e i risultati delle funzioni sono
variabili locali; vengono creati ogni volta che viene chiamata la funzione che li racchiude.
Per esempio, in questo estratto del programma di Lissajous della Sezione 1.4,
per t := 0.0; t < cicli*2*math.Pi; t += res { x :=
math.Sin(t)
y := math.Sin(t*freq + fase) img.SetColorIndex(size+int(x*size+0.5),
size+int(y*size+0.5),
blackIndex)
}

la variabile t viene creata ogni volta che inizia il ciclo for e le nuove variabili x e y vengono create a
ogni iterazione del ciclo.
Come fa il garbage collector a sapere che la memoria di una variabile può essere recuperata? La storia
completa è molto più dettagliata di quanto sia necessario in questa sede, ma l'idea di base è che ogni
variabile a livello di pacchetto e ogni variabile locale di ogni funzione attualmente attiva può
potenzialmente essere l'inizio o la fine di una funzione.

www.it-ebooks.info
36 CAPITOLO 2. STRUTTURA DEL
PROGRAMMA

di un percorso verso la variabile in questione, seguendo i puntatori e altri tipi di riferimenti che portano
alla variabile. Se non esiste un percorso di questo tipo, la variabile è diventata irraggiungibile e non può
più influenzare il resto del calcolo.
Poiché la durata di una variabile è determinata solo dal fatto che sia raggiungibile o meno, una variabile
locale può sopravvivere a una singola iterazione del ciclo che la racchiude. Può continuare a esistere
anche dopo il ritorno della funzione che la racchiude.
Un compilatore può scegliere di allocare le variabili locali sull'heap o sullo stack ma, cosa forse
sorprendente, questa scelta non è determinata dal fatto che per dichiarare la variabile sia stato usato var o
new.
var globale *int

func f() { func g() {


var x int y := nuovo(int)
x = 1 *y = 1
globale = &x }
}

In questo caso, x deve essere allocata su heap perché è ancora raggiungibile dalla variabile global dopo il
ritorno di f, nonostante sia stata dichiarata come variabile locale; diciamo che x sfugge a f. Al contrario,
quando g ritorna, la variabile *y diventa irraggiungibile e può essere riciclata. Poiché *y non sfugge a g, il
compilatore può allocare *y sullo stack, anche se è stata allocata con new. In ogni caso, la nozione di
escape non è qualcosa di cui preoccuparsi per scrivere codice corretto, anche se è bene tenerla presente
durante l'ottimizzazione delle prestazioni, poiché ogni variabile che sfugge richiede un'allocazione di
memoria aggiuntiva.
Il garbage collection è un aiuto enorme per scrivere programmi corretti, ma non solleva dall'onere di
pensare alla memoria. Non è necessario allocare e liberare esplicitamente la memoria, ma per scrivere
programmi efficienti è comunque necessario essere consapevoli della durata delle variabili. Ad esempio,
mantenere puntatori non necessari a oggetti a vita breve all'interno di oggetti a vita lunga, in particolare
variabili globali, impedirà al garbage collector di recuperare gli oggetti a vita breve.

2.4. Incarichi

Il valore di una variabile viene aggiornato da un'istruzione di assegnazione, che nella sua forma più
semplice presenta una variabile a sinistra del segno = e un'espressione a destra.
x = 1 // variabile con nome
*p = true // variabile indiretta
person.name = "bob" // campo struct
count[x] = count[x] * scale // array o slice o elemento della mappa

Ciascuno degli operatori aritmetici e binari bitwise ha un operatore di assegnazione corrispondente


permettendo, ad esempio, di riscrivere l'ultima affermazione come
conteggio[x] *= scala

www.it-ebooks.info
SEZIONE 2.4. ASSEGNAZIONI 37

che ci evita di dover ripetere (e rivalutare) l'espressione per la variabile. Le variabili numeriche possono
anche essere incrementate e decrementate con le istruzioni ++ e --:
v := 1
v++ // come v = v + 1; v diventa 2
v-- // come v = v - 1; v diventa di nuovo 1

2.4.1. Assegnazione di tuple

Un'altra forma di assegnazione, nota come assegnazione a tupla, consente di assegnare più variabili
contemporaneamente. Tutte le espressioni del lato destro vengono valutate prima che le variabili
vengano aggiornate, il che rende questa forma più utile quando alcune variabili appaiono su entrambi i
lati dell'assegnazione, come accade, ad esempio, quando si scambiano i valori di due variabili:
x, y = y, x
a[i], a[j] = a[j], a[i]

o quando si calcola il massimo comun divisore (GCD) di due numeri interi:


func gcd(x, y int) int { for y
!= 0 {
x, y = y, x%y
}
restituire x
}

o quando si calcola l'n-esimo numero di Fibonacci in modo iterativo:


func fib(n int) int { x, y
:= 0, 1
per i := 0; i < n; i++ { x, y
= y, x+y
}
restituire x
}

L'assegnazione di tuple può anche rendere più compatta una sequenza di assegnazioni banali,
i, j, k = 2, 3, 5

Tuttavia, per una questione di stile, evitate la forma a tupla se le espressioni sono complesse; una
sequenza di dichiarazioni separate è più facile da leggere.
Alcune espressioni, come la chiamata a una funzione con più risultati, producono più valori. Quando
una chiamata di questo tipo viene utilizzata in un'istruzione di assegnazione, il lato sinistro deve avere
tante variabili quanti sono i risultati della funzione.
f, err = os.Open("pippo.txt") // la chiamata di funzione restituisce due valori

Spesso le funzioni utilizzano questi risultati aggiuntivi per indicare un qualche tipo di errore, restituendo
un errore come nel caso della chiamata a os.Open, oppure un bool, solitamente chiamato ok.
Come vedremo nei capitoli successivi,

www.it-ebooks.info
38 CAPITOLO 2. STRUTTURA DEL
PROGRAMMA

ci sono tre operatori che a volte si comportano anche in questo modo. Se una ricerca di mappe (§4.3),
un'asserzione di tipo (§7.10) o una ricezione di canali (§8.4.2) appaiono in un'assegnazione in cui sono
attesi due risultati, ciascuno di essi produce un risultato booleano aggiuntivo:
v, ok = m[chiave] // ricerca della mappa
v, ok = x.(T) // asserzione sul tipo
v, ok = <-ch // ricezione del canale

Come per le dichiarazioni di variabili, possiamo assegnare valori indesiderati all'identificatore vuoto:
_, err = io.Copy(dst, src) // scartare il numero di byte
_, ok = x.(T) // controlla il tipo ma scarta il risultato

2.4.2. Assegnabilità

Gli enunciati di assegnazione sono una forma esplicita di assegnazione, ma ci sono molti luoghi in
un programma in cui un'assegnazione avviene implicitamente: una chiamata di funzione assegna
implicitamente i valori degli argomenti alle variabili parametro corrispondenti; un'istruzione di
ritorno assegna implicitamente gli operandi di ritorno alle variabili risultato corrispondenti; e
un'espressione letterale per un tipo composito (§4.2) come questa fetta:
medaglie := []string{"oro", "argento", "bronzo"}.

assegna implicitamente ogni elemento, come se fosse stato scritto in questo modo:
medaglie[0] = "oro"
medaglie[1] = "argento"
medaglie[2] = "bronzo"

Anche gli elementi delle mappe e dei canali, pur non essendo variabili ordinarie, sono soggetti a simili
assegnazioni implicite.

Un'assegnazione, esplicita o implicita, è sempre legale se la parte sinistra (la variabile) e la parte destra (il
valore) hanno lo stesso tipo. Più in generale, l'assegnazione è legale solo se il valore è assegnabile al tipo
della variabile.

La regola dell'assegnabilità ha casi diversi per i vari tipi, quindi spiegheremo il caso pertinente quando
introdurremo ogni nuovo tipo. Per i tipi che abbiamo discusso finora, le regole sono semplici: i tipi
devono corrispondere esattamente e nil può essere assegnato a qualsiasi variabile di tipo interfaccia o
riferimento. Le costanti (§3.6) hanno regole di assegnabilità più flessibili che evitano la necessità di molte
conversioni esplicite.

La possibilità di confrontare due valori con == e != è legata all'assegnabilità: in qualsiasi confronto, il


primo operando deve essere assegnabile al tipo del secondo operando, o viceversa. Come per
l'assegnabilità, spiegheremo i casi rilevanti per la comparabilità quando presenteremo ogni nuovo
tipo.

www.it-ebooks.info
SEZIONE 2.5. DICHIARAZIONI DI TIPO 39

2.5. Dichiarazioni di tipo

Il tipo di una variabile o di un'espressione definisce le caratteristiche dei valori che può assumere, come
la loro dimensione (numero di bit o di elementi, forse), il modo in cui sono rappresentati internamente,
le operazioni intrinseche che possono essere eseguite su di essi e i metodi ad essi associati.
In qualsiasi programma ci sono variabili che condividono la stessa rappresentazione ma che significano
concetti molto diversi. Per esempio, un int può essere usato per rappresentare un indice di ciclo, un
timestamp, un descrittore di file o un mese; un float64 può rappresentare una velocità in metri al
secondo o una temperatura in una delle varie scale; e una stringa può rappresentare una password o il
nome di un colore.
Una dichiarazione di tipo definisce un nuovo tipo denominato che ha lo stesso tipo sottostante di un tipo
esistente. Il tipo denominato fornisce un modo per separare usi diversi e forse incompatibili del tipo
sottostante, in modo che non possano essere mescolati involontariamente.
nome del tipo sottostante

Le dichiarazioni di tipo appaiono più spesso a livello di pacchetto, dove il tipo nominato è visibile in
tutto il pacchetto e, se il nome è esportato (inizia con una lettera maiuscola), è accessibile anche da altri
pacchetti.
Per illustrare le dichiarazioni di tipo, trasformiamo le diverse scale di temperatura in tipi diversi:
gopl.io/ch2/tempconv0
// Il pacchetto tempconv esegue calcoli di temperatura Celsius e Fahrenheit. pacchetto tempconv
importare "fmt"
tipo Celsius float64 tipo
Fahrenheit float64
const (
ZeroC assoluto Celsius = -273,15
CongelamentoC Celsius = 0 BollenteC
Celsius = 100
)
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) } func FToC(f
Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
Questo pacchetto definisce due tipi, Celsius e Fahrenheit, per le due unità di misura della
temperatura. Anche se entrambi hanno lo stesso tipo sottostante, float64, non sono dello stesso tipo,
quindi non possono essere confrontati o combinati in espressioni aritmetiche. La distinzione dei tipi
permette di evitare errori come la combinazione involontaria di temperature in due scale diverse; per
convertire da un float64 è necessaria una conversione di tipo esplicita come Celsius(t) o
Fahrenheit(t). Celsius(t) e Fahrenheit(t) sono conversioni, non chiamate di funzione. Non
cambiano in alcun modo il valore o la rappresentazione, ma rendono esplicito il cambiamento di
significato. D'altra parte, le funzioni CToF e FToC convertono tra le due scale e restituiscono valori
diversi.

www.it-ebooks.info
40 CAPITOLO 2. STRUTTURA DEL
PROGRAMMA

Per ogni tipo T, esiste un'operazione di conversione corrispondente T(x) che converte il valore x nel tipo
T. Una conversione da un tipo a un altro è consentita se entrambi hanno lo stesso tipo sottostante, o se
entrambi sono tipi di puntatori senza nome che puntano a variabili dello stesso tipo sottostante; queste
conversioni cambiano il tipo ma non la rappresentazione del valore. Se x è assegnabile a T, la conversione
è consentita ma di solito è superflua,
Sono consentite anche conversioni tra tipi numerici e tra tipi di stringa e alcuni tipi di slice, come
vedremo nel prossimo capitolo. Queste conversioni possono cambiare la rappresentazione del valore. Ad
esempio, la conversione di un numero in virgola mobile in un intero elimina la parte frazionaria, mentre
la conversione di una stringa in una slice []byte alloca una copia dei dati della stringa. In ogni caso, una
conversione non fallisce mai in fase di esecuzione.
Il tipo sottostante di un tipo chiamato determina la sua struttura e rappresentazione e anche l'insieme di
operazioni intrinseche che supporta, che sono le stesse che si avrebbero se il tipo sottostante fosse usato
direttamente. Ciò significa che gli operatori aritmetici funzionano allo stesso modo per Celsius e Fahren-
heit e per float64, come ci si potrebbe aspettare.

fmt.Printf("%g\n", BollenteC-CongelanteC) // "100" °C


bollenteF := CToF(BollenteC)
fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // "180" °F
fmt.Printf("%g\n", bollenteF-CongelamentoC) // errore di compilazione: mancata
corrispondenza dei tipi

Gli operatori di confronto come == e < possono essere usati anche per confrontare un valore di un tipo
chiamato con un altro dello stesso tipo o con un valore del tipo sottostante. Ma due valori di tipi
nominati diversi non possono essere confrontati direttamente:
var c Celsius var f
Fahrenheit
fmt.Println(c == 0) // "vero"
fmt.Println(f >= 0) // "vero"
fmt.Println(c == f) // errore di compilazione: type
mismatch fmt.Println(c == Celsius(f)) // "vero"!

Osservate attentamente l'ultimo caso. Nonostante il nome, la conversione di tipo Celsius(f) non
cambia il valore del suo argomento, ma solo il suo tipo. Il test è vero perché c e f sono entrambi zero.
Un tipo con nome può fornire una comodità notarile, se aiuta a evitare di scrivere tipi complessi più e
più volte. Il vantaggio è piccolo quando il tipo sottostante è semplice come float64, ma grande per i tipi
complicati, come vedremo quando parleremo delle struct.
I tipi denominati consentono anche di definire nuovi comportamenti per i valori del tipo. Questi
comportamenti sono espressi come un insieme di funzioni associate al tipo, chiamate metodi del tipo.
Vedremo i metodi in dettaglio nel Capitolo 6, ma qui daremo un assaggio del meccanismo.
La dichiarazione seguente, in cui il parametro Celsius c compare prima del nome della funzione,
associa al tipo Celsius un metodo denominato String che restituisce il valore numerico di c seguito
da °C:
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }

www.it-ebooks.info
SEZIONE 2.6. PACCHETTI E FILE 41

Molti tipi dichiarano un metodo String di questa forma, perché controlla il modo in cui i valori del tipo
appaiono quando vengono stampati come stringa dal pacchetto fmt, come vedremo nella Sezione 7.1.
c := FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%v\n", c) // "100°C"; non è necessario chiamare
esplicitamente String fmt.Printf("%s\n", c) // "100°C"
fmt.Println(c) // "100°C"
fmt.Printf("%g\n", c) // "100"; non chiama Stringa
fmt.Println(float64(c)) // "100"; non chiama Stringa

2.6. Pacchetti e file

I pacchetti in Go hanno gli stessi scopi delle librerie o dei moduli in altri linguaggi, supportando la
modularità, l'incapsulamento, la compilazione separata e il riutilizzo. Il codice sorgente di un pacchetto
risiede in uno o più file .go, di solito in una directory il cui nome termina con il percorso di
importazione; per esempio, i file del pacchetto gopl.io/ch1/helloworld sono memorizzati nella
directory
$GOPATH/src/gopl.io/ch1/helloworld.

Ogni pacchetto funge da spazio dei nomi separato per le sue dichiarazioni. All'interno del pacchetto
image, per esempio, l'identificatore Decode si riferisce a una funzione diversa rispetto allo stesso
identificatore nel pacchetto unicode/utf16. Per fare riferimento a una funzione al di fuori del suo
pacchetto, dobbiamo qualificare l'identificatore per rendere esplicito se intendiamo image.Decode o
utf16.Decode.

I pacchetti ci permettono anche di nascondere le informazioni controllando quali nomi sono visibili al di
fuori del pacchetto, o esportati. In Go, una semplice regola regola quali identificatori sono esportati e
quali no: gli identificatori esportati iniziano con una lettera maiuscola.
Per illustrare le basi, supponiamo che il nostro software di conversione della temperatura sia diventato
popolare e che vogliamo renderlo disponibile alla comunità Go come nuovo pacchetto. Come possiamo
farlo?
Creiamo un pacchetto chiamato gopl.io/ch2/tempconv, una variante dell'esempio precedente. (In questo
caso abbiamo fatto un'eccezione alla nostra solita regola di numerare gli esempi in sequenza, in modo
che il percorso del pacchetto possa essere più realistico). Il pacchetto stesso è memorizzato in due file per
mostrare come si accede alle dichiarazioni in file separati di un pacchetto; nella vita reale, un piccolo
pacchetto come questo avrebbe bisogno di un solo file.
Abbiamo inserito le dichiarazioni dei tipi, delle loro costanti e dei loro metodi in tempconv.go:
gopl.io/ch2/tempconv
// Il pacchetto tempconv esegue conversioni Celsius e Fahrenheit. pacchetto
tempconv

importare "fmt"

tipo Celsius float64 tipo


Fahrenheit float64

www.it-ebooks.info
42 CAPITOLO 2. STRUTTURA DEL
PROGRAMMA

const (
ZeroC assoluto Celsius = -273,15
CongelamentoC Celsius = 0 BollenteC
Celsius = 100
)
func (c Celsius) String() stringa { return fmt.Sprintf("%g°C", c) } func
(f Fahrenheit) String() string { return fmt.Sprintf("%g°F", f) }

e le funzioni di conversione in conv.go:


pacchetto tempconv
// CToF converte una temperatura Celsius in Fahrenheit.
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
// FToC converte una temperatura Fahrenheit in Celsius.
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }

Ogni file inizia con una dichiarazione di pacchetto che definisce il nome del pacchetto. Quando il
pacchetto viene importato, i suoi membri vengono indicati come tempconv.CToF e così via. I nomi a
livello di pacchetto, come i tipi e le costanti dichiarati in un file di un pacchetto, sono visibili a tutti gli
altri file del pacchetto, come se il codice sorgente fosse tutto in un unico file. Si noti che tempconv.go
importa fmt, ma conv.go no, perché non usa nulla di fmt.
Poiché i nomi delle cost a livello di pacchetto iniziano con lettere maiuscole, anch'essi sono
accessibili con nomi qualificati come tempconv.AbsoluteZeroC:
fmt.Printf("Brrrr! %v\n", tempconv.AbsoluteZeroC) // "Brrrr! -273.15°C"

Per convertire una temperatura Celsius in Fahrenheit in un pacchetto che importa gopl.io/ch2/temp-
conv, possiamo scrivere il seguente codice:
fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F"

Il commento doc (§10.7.4) che precede immediatamente la dichiarazione del pacchetto documenta il
pacchetto nel suo complesso. Convenzionalmente, dovrebbe iniziare con una frase riassuntiva nello stile
illustrato. Solo un file di ogni pacchetto dovrebbe avere un commento doc del pacchetto. I commenti
doc più estesi sono spesso inseriti in un file a sé stante, convenzionalmente chiamato doc.go.
Esercizio 2.1: Aggiungere tipi, costanti e funzioni a tempconv per elaborare le temperature nella scala
Kelvin, dove lo zero Kelvin è -273,15°C e una differenza di 1K ha la stessa grandezza di 1°C.

2.6.1. Importazioni

All'interno di un programma Go, ogni pacchetto è identificato da una stringa unica chiamata
percorso di importazione. Queste sono le stringhe che appaiono in una dichiarazione di importazione
come "gopl.io/ch2/tempconv". Le specifiche del linguaggio non definiscono la provenienza o il significato
di queste stringhe; spetta agli strumenti interpretarle. Quando si usa lo strumento go (Capitolo 10),
un percorso di importazione denota una directory contenente uno o più file sorgente Go che insieme
costituiscono il pacchetto.

www.it-ebooks.info
SEZIONE 2.6. PACCHETTI E FILE 43

Oltre al percorso di importazione, ogni pacchetto ha un nome di pacchetto, che è il nome breve (e
non necessariamente unico) che appare nella sua dichiarazione di pacchetto. Per convenzione, il nome
di un pacchetto corrisponde all'ultimo segmento del suo percorso di importazione, il che rende
facile prevedere che il nome del pacchetto di gopl.io/ch2/tempconv sia tempconv.
Per usare gopl.io/ch2/tempconv, dobbiamo i m p o r t a r l o :
gopl.io/ch2/cf
// Cf converte il suo argomento numerico in Celsius e Fahrenheit. pacchetto
main
importare (
"fmt"
"os"
"strconv"
"gopl.io/ch2/tempconv"
)
func main() {
per _, arg := gamma os.Args[1:] {
t, err := strconv.ParseFloat(arg, 64) if
err != nil {
fmt.Fprintf(os.Stderr, "cf: %v\n", err) os.Exit(1)
}
f := tempconv.Fahrenheit(t) c :=
tempconv.Celsius(t)
fmt.Printf("%s = %s, %s = %s\n",
f, tempconv.FToC(f), c, tempconv.CToF(c))
}
}

La dichiarazione di importazione lega un nome breve al pacchetto importato, che può essere usato per
fare riferimento al suo contenuto in tutto il file. L'importazione di cui sopra ci permette di fare
riferimento a nomi all'interno di gopl.io/ch2/tempconv usando un identificatore qualificato come
tempconv.CToF. Per impostazione predefinita, il nome breve è il nome del pacchetto - in questo caso
tempconv - ma una dichiarazione di importazione può specificare un nome alternativo per evitare
un conflitto (§10.3).
Il programma cf converte un singolo argomento numerico da riga di comando nel suo valore sia in
Celsius che in Fahrenheit:
$ go build gopl.io/ch2/cf
$ ./cf 32
32°F = 0°C, 32°C = 89,6°F
$ ./cf 212
212°F = 100°C, 212°C = 413,6°F
$ ./cf -40
-40°F = -40°C, -40°C = -40°F

È un errore importare un pacchetto e poi non farvi riferimento. Questo controllo aiuta a eliminare le
dipendenze che diventano superflue con l'evoluzione del codice, anche se può essere una seccatura
durante la fase di

www.it-ebooks.info
44 CAPITOLO 2. STRUTTURA DEL
PROGRAMMA

debugging, poiché il commento di una riga di codice come log.Print("got here!") può rimuovere
l'unico riferimento al nome del pacchetto log, causando l'emissione di un errore da parte del
compilatore. In questo caso, è necessario commentare o eliminare l'importazione non necessaria.
Meglio ancora, usare lo strumento golang.org/x/tools/cmd/goimports, che inserisce e rimuove
automaticamente i pacchetti dalla dichiarazione di importazione, come necessario; la maggior parte
degli editor può essere configurata per eseguire goimports ogni volta che si salva un file. Come lo
strumento gofmt, stampa anche i file sorgenti di Go nel formato canonico.
Esercizio 2.2: Scrivere un programma generico di conversione di unità di misura analogo a cf che legga i
numeri dagli argomenti della riga di comando o dallo standard input se non ci sono argomenti, e
converta ogni numero in unità di misura come la temperatura in Celsius e Fahrenheit, la lunghezza in
piedi e metri, il peso in libbre e chilogrammi e simili.

2.6.2. Inizializzazione del pacchetto

L'inizializzazione del pacchetto inizia inizializzando le variabili a livello di pacchetto nell'ordine in cui
sono state dichiarate, con l'eccezione che le dipendenze vengono risolte per prime:
var a = b + c // a inizializzato terzo, a 3
var b = f() // b inizializzato per secondo, a 2, dalla
chiamata a f var c = 1 // c inizializzato per primo, a 1
func f() int { restituisce c + 1 }

Se il pacchetto ha più file .go, questi vengono inizializzati nell'ordine in cui vengono forniti al
compilatore; lo strumento go ordina i file .go per nome prima di invocare il compilatore.
Ogni variabile dichiarata a livello di pacchetto inizia con il valore della sua espressione inizializzatrice, se
presente, ma per alcune variabili, come le tabelle di dati, un'espressione inizializzatrice potrebbe non
essere il modo più semplice per impostare il valore iniziale. In questo caso, il meccanismo della funzione
init può essere più semplice. Ogni file può contenere un numero qualsiasi di funzioni la cui
dichiarazione è semplicemente
func init() { /* ... */ }

Tali funzioni di init non possono essere chiamate o referenziate, ma per il resto sono funzioni
normali. All'interno di ogni file, le funzioni di init vengono eseguite automaticamente all'avvio del
programma, nell'ordine in cui sono state dichiarate.
Un pacchetto viene inizializzato alla volta, secondo l'ordine di importazione nel programma, prima le
dipendenze, in modo che un pacchetto p che importa q possa essere sicuro che q sia completamente
inizializzato prima che inizi la sua inizializzazione. L'inizializzazione procede dal basso verso l'alto; il
pacchetto principale è l'ultimo a essere inizializzato. In questo modo, tutti i pacchetti sono completamente
inizializzati prima che inizi la funzione principale dell'applicazione.
Il pacchetto sottostante definisce una funzione PopCount che restituisce il numero di bit impostati,
cioè di bit il cui valore è 1, in un valore uint64, che viene chiamato conteggio della popolazione.
Utilizza una funzione init per precompilare una tabella di risultati, pc, per ogni possibile valore a 8 bit,
in modo che la funzione PopCount non debba fare 64 passi, ma possa semplicemente restituire la somma
di otto ricerche nella tabella. (Questo non è certamente l'algoritmo più veloce per il conteggio dei bit,
ma è comodo per illustrare l'init

www.it-ebooks.info
SEZIONE 2.7. AMBITO DI 45
APPLICAZIONE

e per aver mostrato come precompilare una tabella di valori, una tecnica di programmazione spesso utile).
gopl.io/ch2/popcount
pacchetto popcount
// pc[i] è il numero di abitanti di i. var pc
[256]byte
func init() {
per i := range pc {
pc[i] = pc[i/2] + byte(i&1)
}
}
// PopCount restituisce il numero di abitanti (numero di bit impostati) di x. func
PopCount(x uint64) int {
restituire int(pc[byte(x>>(0*8))] +
pc[byte(x>>(1*8))] +
pc[byte(x>>(2*8)) +
pc[byte(x>>(3*8)) +
pc[byte(x>>(4*8)) +
pc[byte(x>>(5*8)) +
pc[byte(x>>(6*8)) +
pc[byte(x>>(7*8))])
}

Si noti che il ciclo di range init utilizza solo l'indice; il valore non è necessario e quindi non deve
essere incluso. Il ciclo avrebbe potuto essere scritto anche come
per i, _ := range pc {

Vedremo altri usi delle funzioni di init nella prossima sezione e nella sezione 10.5.
Esercizio 2.3: Riscrivere PopCount per utilizzare un ciclo invece di una singola espressione. Confrontare
le prestazioni delle due versioni. (La sezione 11.4 mostra come confrontare sistematicamente le
prestazioni di diverse implementazioni).
Esercizio 2.4: Scrivete una versione di PopCount che conta i bit spostando il suo argomento attraverso 64
posizioni di bit, verificando ogni volta il bit più a destra. Confrontate le sue prestazioni con quelle della
versione con ricerca in tabella.
Esercizio 2.5: L'espressione x&(x-1) cancella il bit non nullo più a destra di x. Scrivete una versione
di PopCount che conta i bit utilizzando questo fatto e valutatene le prestazioni.

2.7. Ambito di applicazione

Una dichiarazione associa un nome a un'entità del programma, come una funzione o una variabile.
L'ambito di una dichiarazione è la parte del codice sorgente in cui un uso del nome dichiarato si riferisce
a quella dichiarazione.

www.it-ebooks.info
46 CAPITOLO 2. STRUTTURA DEL
PROGRAMMA

Non confondere l'ambito con il tempo di vita. L'ambito di una dichiarazione è una regione del testo del
programma; è una proprietà a tempo di compilazione. Il tempo di vita di una variabile è l'intervallo di
tempo durante l'esecuzione in cui la variabile può essere citata da altre parti del programma; è una
proprietà di run-time.
Un blocco sintattico è una sequenza di istruzioni racchiuse tra parentesi graffe, come quelle che
circondano il corpo di una funzione o di un ciclo. Un nome dichiarato all'interno di un blocco sintattico
non è visibile al di fuori di esso. Il blocco racchiude le sue dichiarazioni e ne determina l'ambito.
Possiamo generalizzare questa nozione di blocco per includere altri raggruppamenti di dichiarazioni che
non sono esplicitamente circondate da parentesi graffe nel codice sorgente; li chiameremo tutti blocchi
lessicali. Esiste un blocco lessicale per l'intero codice sorgente, chiamato blocco universo; per ogni
pacchetto; per ogni file; per ogni istruzione for, if e switch; per ogni caso in un'istruzione switch o select; e,
naturalmente, per ogni blocco sintattico esplicito.
Il blocco lessicale di una dichiarazione determina il suo ambito, che può essere grande o piccolo. Le
dichiarazioni dei tipi built-in, delle funzioni e delle costanti come int, len e true si trovano nel blocco
universo e possono essere richiamate in tutto il programma. Le dichiarazioni al di fuori di qualsiasi
funzione, cioè a livello di pacchetto, possono essere richiamate da qualsiasi file dello stesso
pacchetto. I pacchetti importati, come fmt nell'esempio di tempconv, sono dichiarati a livello di file,
quindi possono essere richiamati dallo stesso file, ma non da un altro file dello stesso pacchetto senza
un'altra importazione. Molte dichiarazioni, come quella della variabile c nella funzione tempconv.CToF,
sono locali, quindi possono essere richiamate solo all'interno della stessa funzione o forse solo da una
parte di essa.
L'ambito di un'etichetta di flusso di controllo, utilizzata dalle istruzioni break, continue e goto, è l' intera
funzione che la racchiude.
Un programma può contenere più dichiarazioni dello stesso nome, purché ogni dichiarazione si trovi in
un blocco lessicale diverso. Ad esempio, è possibile dichiarare una variabile locale con lo stesso nome di
una variabile a livello di pacchetto. Oppure, come mostrato nella Sezione 2.3.3, si può dichiarare un
parametro di funzione chiamato new, anche se una funzione con questo nome è predichiarata nel blocco
universe. Non bisogna però esagerare: più ampio è l'ambito della ridichiarazione, più è probabile che si
sorprenda il lettore.
Quando il compilatore incontra un riferimento a un nome, cerca una dichiarazione, a partire dal blocco
lessicale più interno e fino al blocco universo. Se il compilatore non trova alcuna dichiarazione, segnala
un errore di ''nome non dichiarato''. Se un nome è dichiarato sia in un blocco esterno che in un blocco
interno, la dichiarazione interna viene trovata per prima. In questo caso, si dice che la dichiarazione
interna ombreggia o nasconde quella esterna, rendendola inaccessibile:
func f() {} var

g = "g" func

main() {
f := "f"
fmt.Println(f) // "f"; var locale f ombreggia func a livello di pacchetto f fmt.Println(g)
// "g"; var a livello di pacchetto
fmt.Println(h) // errore di compilazione: undefined: h
}

www.it-ebooks.info
SEZIONE 2.7. AMBITO DI 47
APPLICAZIONE

All'interno di una funzione, i blocchi lessicali possono essere annidati a una profondità arbitraria, in
modo che una dichiarazione locale possa fare da ombra a un'altra. La maggior parte dei blocchi sono
creati da costrutti di flusso di controllo come le istruzioni if e i cicli for. Il programma che segue ha
tre diverse variabili chiamate x perché ogni dichiarazione appare in un blocco lessicale diverso.
(Questo esempio illustra le regole di scope, non il buon stile).
func main() {
x := "ciao!"
for i := 0; i < len(x); i++ { x :=
x[i]
se x != '!' {
x := x + 'A' - 'a'
fmt.Printf("%c", x) // "CIAO" (una lettera per iterazione)
}
}
}

Le espressioni x[i] e x + 'A' - 'a' si riferiscono ciascuna a una dichiarazione di x da un blocco


esterno; lo spiegheremo tra poco. (Si noti che quest'ultima espressione non è equivalente a uni-
code.ToUpper).

Come già detto, non tutti i blocchi lessicali corrispondono a sequenze esplicite di istruzioni delimitate da
parentesi graffe; alcuni sono semplicemente impliciti. Il ciclo for qui sopra crea due blocchi lessicali: il
blocco esplicito per il corpo del ciclo e un blocco implicito che racchiude anche le variabili dichiarate
dalla clausola di inizializzazione, come i. L'ambito di una variabile dichiarata nel blocco implicito è la
condizione, il post-statement (i++) e il corpo dell'istruzione for.

Anche l'esempio seguente ha tre variabili di nome x, ciascuna dichiarata in un blocco diverso: uno nel
corpo della funzione, uno nel blocco dell'istruzione for e uno nel corpo del ciclo, ma solo due dei blocchi
sono espliciti:
func main() {
x := "ciao"
per _, x := intervallo x { x
:= x + 'A' - 'a'
fmt.Printf("%c", x) // "CIAO" (una lettera per iterazione)
}
}

Come i cicli for, anche gli if e gli switch creano blocchi impliciti oltre ai loro blocchi corpo. Il codice
della seguente catena if-else mostra il campo di applicazione di x e y:
if x := f(); x == 0 { fmt.Println(x)
} else if y := g(x); x == y { fmt.Println(x, y)
} else {
fmt.Println(x, y)
}
fmt.Println(x, y) // errore di compilazione: x e y non sono visibili qui

www.it-ebooks.info
48 CAPITOLO 2. STRUTTURA DEL
PROGRAMMA

Il secondo if è annidato all'interno del primo, quindi le variabili dichiarate nell'inizializzatore del primo
stato sono visibili nel secondo. Regole simili si applicano a ogni caso di un'istruzione switch: c'è un
blocco per la condizione e un blocco per ogni corpo del caso.

A livello di pacchetto, l'ordine in cui compaiono le dichiarazioni non ha effetto sul loro ambito, quindi
una dichiarazione può riferirsi a se stessa o a un'altra che la segue, consentendo di dichiarare tipi e
funzioni ricorsivi o mutuamente ricorsivi. Tuttavia, il compilatore segnalerà un errore se una
dichiarazione di costante o di variabile si riferisce a se stessa.

In questo programma:
if f, err := os.Open(fname); err := nil { // errore di compilazione: inutilizzato: f
return err
}
f.ReadByte() // errore di compilazione:
undefined f f.Close() // errore di
compilazione: non definito f

l'ambito di f è solo l'istruzione if, quindi f non è accessibile alle istruzioni successive, con
conseguenti errori del compilatore. A seconda del compilatore, si potrebbe ottenere un ulteriore
errore che segnala che la variabile locale f non è mai stata utilizzata.

Pertanto, spesso è necessario dichiarare f prima della condizione, in modo che sia accessibile dopo:
f, err := os.Open(fname) if
err := nil {
restituire err
}
f.ReadByte()
f.Close()

Si potrebbe essere tentati di evitare di dichiarare f ed err nel blocco esterno, spostando le chiamate a
ReadByte e Close all'interno di un blocco else:

if f, err := os.Open(fname); err := nil { return err


} else {
// f e err sono visibili anche qui f.ReadByte()
f.Close()
}

ma la prassi normale in Go è quella di gestire l'errore nel blocco if e poi ritornare, in modo che il
percorso di esecuzione corretto non sia rientrato.

Le dichiarazioni di variabili brevi richiedono la consapevolezza dell'ambito. Si consideri il programma


seguente, che inizia ottenendo la directory di lavoro corrente e salvandola in una variabile a livello di
pacchetto. Questo potrebbe essere fatto chiamando os.Getwd nella funzione main, ma sarebbe meglio
separare questa preoccupazione dalla logica primaria, soprattutto se il mancato ottenimento della
directory è un errore fatale. La funzione log.Fatalf stampa un messaggio e chiama os.Exit(1).

www.it-ebooks.info
SEZIONE 2.7. AMBITO DI 49
APPLICAZIONE

var cwd stringa


func init() {
cwd, err := os.Getwd() // errore di compilazione: inutilizzato:
cwd if err := nil {
log.Fatalf("os.Getwd non è riuscito: %v", err)
}
}

Poiché né cwd né err sono già dichiarati nel blocco della funzione init, l'istruzione := li dichiara
entrambi come variabili locali. La dichiarazione interna di cwd rende inaccessibile quella esterna,
quindi l'istruzione non aggiorna la variabile cwd a livello di pacchetto come previsto.
Gli attuali compilatori di Go rilevano che la variabile cwd locale non viene mai utilizzata e la segnalano
come errore, ma non sono strettamente obbligati a eseguire questo controllo. Inoltre, una piccola
modifica, come l'aggiunta di un'istruzione di logging che faccia riferimento alla cwd locale,
vanificherebbe il controllo.
var cwd string

func init() {
cwd, err := os.Getwd() // NOTA: sbagliato!
if err != nil {
log.Fatalf("os.Getwd non è riuscito: %v", err)
}
log.Printf("Directory di lavoro = %s", cwd)
}

La variabile globale cwd rimane non inizializzata e l'output di log apparentemente normale nasconde il bug.
Ci sono diversi modi per affrontare questo potenziale problema. Il più diretto è evitare :=
dichiarando err in una dichiarazione var separata:
var cwd string

func init() {
var err errore
cwd, err = os.Getwd() if
err != nil {
log.Fatalf("os.Getwd non è riuscito: %v", err)
}
}

Abbiamo visto come i pacchetti, i file, le dichiarazioni e le affermazioni esprimono la struttura dei
programmi. Nei prossimi due capitoli vedremo la struttura dei dati.

www.it-ebooks.info
Questa pagina è stata lasciata intenzionalmente in bianco

www.it-ebooks.info
3
Tipi di dati di base

Naturalmente si tratta di bit, ma i computer operano fondamentalmente su numeri di dimensioni fisse


chiamati parole, che vengono interpretate come numeri interi, numeri in virgola mobile, insiemi di bit o
indirizzi di memoria e poi combinate in aggregati più grandi che rappresentano pacchetti, pixel,
portafogli, poesie e quant'altro. Go offre una varietà di modi per organizzare i dati, con uno spettro di
tipi di dati che da un lato corrispondono alle caratteristiche dell'hardware e dall'altro forniscono ciò che
serve ai programmatori per rappresentare comodamente strutture di dati complicate.

I tipi di Go si dividono in quattro categorie: tipi di base, tipi aggregati, tipi di riferimento e tipi di
interfaccia. I tipi base, argomento di questo capitolo, comprendono numeri, stringhe e booleani. I tipi
aggregati - gli array (§4.1) e le strutture (§4.4) - formano tipi di dati più complicati combinando i valori
di diversi tipi più semplici. I tipi di riferimento sono un gruppo eterogeneo che comprende i puntatori
(§2.3.2), le fette (§4.2), le mappe (§4.3), le funzioni (Capitolo 5) e i canali (Capitolo 8), ma ciò che hanno
in comune è che si riferiscono tutti a variabili di programma o di stato indirettamente, in modo che
l'effetto di un'operazione applicata a un riferimento sia osservato da tutte le copie di quel riferimento.
Infine, nel Capitolo 7 parleremo dei tipi di interfaccia.

3.1. Interi

I tipi di dati numerici di Go includono diverse dimensioni di numeri interi, numeri in virgola mobile e
numeri complessi. Ogni tipo numerico determina la dimensione e il segno dei suoi valori. Cominciamo
con i numeri interi.

Go fornisce l'aritmetica dei numeri interi sia firmati che non firmati. Esistono quattro dimensioni
distinte di interi firmati - 8, 16, 32 e 64 bit - rappresentate dai tipi int8, int16, int32 e int64, e le
corrispondenti versioni senza segno uint8, uint16, uint32 e uint64.

51

www.it-ebooks.info
52 CAPITOLO 3. TIPI DI DATI DI BASE

Esistono anche due tipi, chiamati solo int e uint, che rappresentano la dimensione naturale o più
efficiente per gli interi con e senza segno su una particolare piattaforma; int è di gran lunga il tipo
numerico più utilizzato. Entrambi questi tipi hanno la stessa dimensione, 32 o 64 bit, ma non bisogna
fare ipotesi su quale sia; compilatori diversi possono fare scelte diverse anche su hardware identico.
Il tipo runa è un sinonimo di int32 e indica convenzionalmente che un valore è un punto di codice
Unicode. I due nomi possono essere usati in modo intercambiabile. Allo stesso modo, il tipo byte è un
sinonimo di uint8 e sottolinea che il valore è un pezzo di dati grezzi piuttosto che una piccola quantità
numerica.
Infine, esiste un tipo intero senza segno uintptr, la cui larghezza non è specificata ma è sufficiente a
contenere tutti i bit di un valore di puntatore. Il tipo uintptr viene utilizzato solo per la programmazione
a basso livello, come ad esempio al confine tra un programma Go e una libreria C o un sistema
operativo. Ne vedremo degli esempi quando tratteremo il pacchetto unsafe nel Capitolo 13.
Indipendentemente dalla loro dimensione, int, uint e uintptr sono tipi diversi dai loro fratelli a
dimensione esplicita. Pertanto, int non è lo stesso tipo di int32, anche se la dimensione naturale
degli interi è di 32 bit, ed è necessaria una conversione esplicita per utilizzare un valore int dove è
necessario un int32 e viceversa.
I numeri con segno sono rappresentati in forma di complemento a 2, in cui il bit di ordine superiore è
riservato al segno del numero e l'intervallo di valori di un numero a n bit è compreso tra -2n-1 e 2n-1-1. I
numeri interi senza segno utilizzano l'intera gamma di bit per i valori non negativi e quindi hanno un
intervallo compreso tra 0 e 2n-1. Ad esempio, l'intervallo di int8 va da -128 a 127, mentre l'intervallo di
uint8 va da 0 a 255.
Gli operatori binari di Go per l'aritmetica, la logica e il confronto sono elencati in ordine di precedenza
decrescente:
* / % << >> & &^
+ - | ^
== != < <= > >=
&&
||

Esistono solo cinque livelli di precedenza per gli operatori binari. Gli operatori dello stesso livello si
associano a sinistra, quindi le parentesi possono essere necessarie per chiarezza o per far sì che gli
operatori vengano valutati nell'ordine previsto in un'espressione come maschera & (1 << 28).
Ogni operatore nelle prime due righe della tabella precedente, ad esempio +, ha un operatore corrispondente
operatore di assegnazione come += che può essere usato per abbreviare un'istruzione di assegnazione.
Gli operatori aritmetici interi +, -, * e / possono essere applicati a numeri interi, a virgola mobile e
complessi, ma l'operatore di resto % si applica solo ai numeri interi. Il comportamento di % per i numeri
negativi varia a seconda del linguaggio di programmazione. In Go, il segno del resto è sempre uguale al
segno del dividendo, quindi -5%3 e -5%-3 sono entrambi -2. Il comportamento di / dipende dal fatto che i
suoi operandi siano numeri interi, quindi 5,0/4,0 è 1,25, ma 5/4 è 1 perché la divisione intera tronca il
risultato verso lo zero.

www.it-ebooks.info
SEZIONE 3.1. INTEGRATORI 53

Se il risultato di un'operazione aritmetica, con o senza segno, ha più bit di quanti ne possano essere
rappresentati nel tipo di risultato, si parla di overflow. I bit di ordine superiore che non si adattano
vengono scartati silenziosamente. Se il numero originale è di tipo firmato, il risultato potrebbe essere
negativo se il bit più a sinistra è un 1, come nell'esempio int8 qui riportato:
var u uint8 = 255
fmt.Println(u, u+1, u*u) // "255 0 1"

var i int8 = 127


fmt.Println(i, i+1, i*i) // "127 -128 1"

Due numeri interi dello stesso tipo possono essere confrontati utilizzando gli operatori di confronto binario
riportati di seguito; il tipo di un'espressione di confronto è un booleano.
== uguale a
!= non uguale a
< meno di
<= inferiore o uguale a
> maggiore di
>= maggiore o uguale a
Infatti, tutti i valori di tipo base - booleani, numeri e stringhe - sono comparabili, il che significa che
due valori dello stesso tipo possono essere confrontati utilizzando gli operatori == e !=. Inoltre, i
numeri interi, i numeri in virgola mobile e le stringhe sono ordinati dagli operatori di confronto. I valori
di molti altri tipi non sono confrontabili e nessun altro tipo è ordinato. Man mano che
incontreremo ciascun tipo, presenteremo le regole che governano la comparabilità dei suoi valori.
Esistono anche operatori unari di addizione e sottrazione:
+ positivo unario (nessun effetto)
- negazione unaria
Per i numeri interi, +x è un'abbreviazione di 0+x e -x è un'abbreviazione di 0-x; per i numeri in virgola mobile e
i numeri complessi, +x è semplicemente x e -x è la negazione di x.
Go fornisce anche i seguenti operatori binari bitwise, i primi quattro dei quali trattano i loro op- portatori
come modelli di bit, senza alcun concetto di riporto o segno aritmetico:
& AND bitwise
| OR bitwise
^ XOR bitwise
&^ bit chiaro (AND NOT)
<< spostamento a sinistra
>> spostamento a destra
L'operatore ^ è un OR esclusivo bitwise (XOR) se usato come operatore binario, ma se usato come
operatore prefisso unario è una negazione o un complemento bitwise; cioè, restituisce un valore con
ogni bit del suo operando invertito. L'operatore &^ è bit clear (AND NOT): nell'espressione z = x &^ y,
ciascun bit di z è 0 se il corrispondente bit di y è 1; altrimenti è uguale al bit corrispondente di x.

www.it-ebooks.info
54 CAPITOLO 3. TIPI DI DATI DI BASE

Il codice seguente mostra come le operazioni bitwise possano essere utilizzate per interpretare un valore
uint8 come un insieme compatto ed efficiente di 8 bit indipendenti. Utilizza il verbo %b di Printf per
stampare le cifre binarie di un numero; 08 modifica %b (un avverbio!) per riempire il risultato di zeri fino
a 8 cifre esatte.
var x uint8 = 1<<1 | 1<<5 var y
uint8 = 1<<1 | 1<<2
fmt.Printf("%08b\n", x) // "00100010", l'insieme {1, 5}
fmt.Printf("%08b\n", y) // "00000110", l'insieme {1, 2}
fmt.Printf("%08b\n", x&y) // "00000010", l'intersezione {1} fmt.Printf("%08b\n", x|y) //
"00100110", l'unione {1, 2, 5}
fmt.Printf("%08b\n", x^y) // "00100100", la differenza simmetrica {2, 5} fmt.Printf("%08b\n",
x&^y) // "00100000", la differenza {5}
for i := uint(0); i < 8; i++ {
if x&(1<<i) != 0 { // test di appartenenza
fmt.Println(i) // "1", "5"
}
}
fmt.Printf("%08b\n", x<<1) // "01000100", l'insieme {2, 6}
fmt.Printf("%08b\n", x>>1) // "00010001", l'insieme {0, 4}

(La sezione 6.5 mostra un'implementazione di insiemi di interi che possono essere molto più grandi di un
byte).
Nelle operazioni di spostamento x<<n e x>>n, l'operando n determina il numero di posizioni di bit da
spostare e deve essere senza segno; l'operando x può essere senza segno o firmato. Aritmeticamente, uno
shift a sinistra x<<n è equivalente alla moltiplicazione per 2n e uno shift a destra x>>n è equivalente al
piano della divisione per 2 .n
Gli spostamenti a sinistra riempiono i bit lasciati liberi con zeri, così come gli spostamenti a destra dei
numeri senza segno, ma gli spostamenti a destra dei numeri firmati riempiono i bit lasciati liberi con
copie del bit di segno. Per questo motivo, è importante utilizzare l'aritmetica senza segno quando si
tratta un intero come un modello di bit.
Sebbene Go fornisca numeri e aritmetica senza segno, si tende a usare la forma int firmata anche per
quantità che non possono essere negative, come la lunghezza di un array, anche se uint potrebbe
sembrare una scelta più ovvia. Infatti, la funzione built-in len restituisce un int con segno, come in
questo ciclo che annuncia le medaglie dei premi in ordine inverso:
medaglie := []stringa{"oro", "argento", "bronzo"}
per i := len(medaglie) - 1; i >= 0; i-- {
fmt.Println(medaglie[i]) // "bronzo", "argento", "oro"
}

L'alternativa sarebbe calamitosa. Se len restituisse un numero senza segno, allora anche i sarebbe un uint e
la condizione i >= 0 sarebbe sempre vera per definizione. Dopo la terza iterazione, in cui i == 0,
l'istruzione i-- farebbe sì che i non diventi -1, ma il valore massimo di uint (per esempio, 264-1), e la
valutazione di medaglie[i] fallirebbe a tempo di esecuzione, o andrebbe in panico (§5.9), tentando di
accedere a un elemento al di fuori dei limiti della slice.
Per questo motivo, i numeri senza segno tendono a essere utilizzati solo quando sono richiesti i loro
operatori bitwise o operatori aritmetici particolari, come nell'implementazione di insiemi di bit, nel
parsing di file binari e nella gestione di file di tipo "binario".

www.it-ebooks.info
SEZIONE 3.1. INTEGRATORI 55

formati, o per l'hashing e la crittografia. In genere non vengono utilizzati per quantità semplicemente
non negative.

In generale, per convertire un valore da un tipo a un altro è necessaria una conversione esplicita e gli
operatori binari per l'aritmetica e la logica (tranne gli shift) devono avere operandi dello stesso tipo.
Sebbene questo comporti occasionalmente espressioni più lunghe, elimina un'intera classe di problemi e
rende i programmi più facili da capire.

Come esempio noto in altri contesti, si consideri questa sequenza:


var mele int32 = 1 var
arance int16 = 2
var composta int = mele + arance // errore di compilazione

Il tentativo di compilare queste tre dichiarazioni produce un messaggio di errore:


operazione non valida: mele + arance (tipi non corrispondenti int32 e int16)

Questo disallineamento di tipo può essere risolto in diversi modi, il più diretto dei quali è la conversione
di tutto in un tipo comune:
var composta = int(mele) + int(arance)

Come descritto nella Sezione 2.5, per ogni tipo T, l'operazione di conversione T(x) converte il valore x nel
tipo T, se la conversione è consentita. Molte conversioni da intero a intero non comportano alcuna
modifica del valore, ma indicano semplicemente al compilatore come interpretare un valore. Ma una
conversione che trasforma un intero grande in uno più piccolo, o una conversione da intero a virgola
mobile o viceversa, può modificare il valore o perdere precisione:
f := 3,141 // un float64 i
:= int(f)
fmt.Println(f, i) // "3.141 3"
f = 1.99
fmt.Println(int(f)) // " 1"

La conversione da float a intero scarta qualsiasi parte frazionaria, troncando verso lo zero. Si dovrebbero
evitare le conversioni in cui l'operando è fuori dall'intervallo del tipo di destinazione, perché il
comportamento dipende dall'implementazione:
f := 1e100 // un float64
i := int(f) // il risultato dipende dall'implementazione

I letterali interi di qualsiasi dimensione e tipo possono essere scritti come numeri decimali ordinari, o
come numeri ottali se iniziano con 0, come in 0666, o come esadecimali se iniziano con 0x o 0X, come in
0xdeadbeef. Le cifre esadecimali possono essere m a i u s c o l e o minuscole. Oggi i numeri ottali
sembrano essere utilizzati per un solo scopo: i permessi dei file sui sistemi POSIX, ma i numeri
esadecimali sono ampiamente utilizzati per enfatizzare il modello di bit di un numero piuttosto che il
suo valore numerico.
Quando si stampano i numeri usando il pacchetto fmt, si può controllare il radix e il formato con l'opzione
verbi %d, %o e %x, come mostrato in questo esempio:

www.it-ebooks.info
56 CAPITOLO 3. TIPI DI DATI DI BASE

o := 0666
fmt.Printf("%d %[1]o %#[1]o\n", o) // "438 666 0666" x :=
int64(0xdeadbeef)
fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)
// Uscita:
// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF

Notate l'uso di due trucchi di fmt. Normalmente una stringa di formato Printf contenente più verbi %
richiederebbe lo stesso numero di operandi extra, ma gli ''avverbi'' [1] dopo % dicono a Printf di usare il
primo operando più volte. In secondo luogo, l'avverbio # per %o o %x o %X indica a Printf di emettere un
prefisso 0 o 0x o 0X rispettivamente.
I letterali delle rune sono scritti come caratteri all'interno di apici singoli. L'esempio più semplice è un
carattere ASCII come 'a', ma è possibile scrivere qualsiasi punto di codice Unicode direttamente o con
escape numerici, come vedremo tra poco.
Le rune vengono stampate con %c, o con %q se si desidera la citazione:
ascii := 'a'
unicode := 'D'
newline := '\n'
fmt.Printf("%d %[1]c %[1]q\n", ascii) // "97 a 'a'"
fmt.Printf("%d %[1]c %[1]q\n", unicode) // "22269 D 'D'"
fmt.Printf("%d %[1]q\n", newline) // "10 '\n'"

3.2. Numeri in virgola mobile

Go fornisce due dimensioni di numeri in virgola mobile, float32 e float64. Le loro proprietà
aritmetiche sono regolate dallo standard IEEE 754, implementato da tutte le moderne CPU.
I valori di questi tipi numerici vanno da piccoli a enormi. I limiti dei valori in virgola mobile si trovano
nel pacchetto math. La costante math.MaxFloat32, il più grande float32, è circa 3,4e38, mentre
math.MaxFloat64 è circa 1,8e308. I valori positivi più piccoli sono vicini a
1,4e-45 e 4,9e-324, rispettivamente.
Un float32 fornisce circa sei cifre decimali di precisione, mentre un float64 ne fornisce circa 15;
il float64 dovrebbe essere preferito per la maggior parte degli scopi, perché i calcoli con float32
accumulano rapidamente errori a meno che non si faccia molta attenzione, e il più piccolo intero
positivo che non può essere rappresentato esattamente come un float32 non è grande:
var f float32 = 16777216 // 1 << 24
fmt.Println(f == f+1) // "vero"!

I numeri in virgola mobile possono essere scritti letteralmente usando i decimali, come in questo caso:
const e = 2,71828 // (approssimativamente)

Le cifre possono essere omesse prima della virgola (.707) o dopo (1.). I numeri molto piccoli o molto
grandi si scrivono meglio in notazione scientifica, con la lettera e o E che precede l'esponente decimale:

www.it-ebooks.info
SEZIONE 3.2. NUMERI IN VIRGOLA MOBILE 57

const Avogadro = 6,02214129e23 const


Planck = 6.62606957e-34

I valori in virgola mobile vengono stampati comodamente con il verbo %g di Printf, che sceglie la
rappresentazione più compatta con una precisione adeguata, ma per le tabelle di dati possono essere più
appropriate le forme %e (esponente) o %f (senza esponente). Tutti e tre i verbi consentono di controllare
la larghezza del campo e la precisione numerica.
per x := 0; x < 8; x++ {
fmt.Printf("x = %d e* = %8.3f\n", x, math.Exp(float64(x))
}

Il codice precedente stampa le potenze di e con tre cifre decimali di precisione, allineate in un campo di
otto caratteri:
x = 0 e* = 1.000
x = 1 e* = 2.718
x = 2 e* = 7.389
x = 3 e* = 20.086
x = 4 e* = 54.598
x = 5 e* = 148.413
x = 6 e* = 403.429
x = 7 e* = 1096.633

Oltre a un'ampia raccolta delle consuete funzioni matematiche, il pacchetto math dispone di
funzioni per la creazione e il rilevamento dei valori speciali definiti dall'IEEE 754: gli infiniti positivi
e negativi, che rappresentano numeri di grandezza eccessiva e il risultato della divisione per zero; e NaN
(''non un numero''), il risultato di operazioni matematicamente dubbie come 0/0 o Sqrt(-1).
var z float64
fmt.Println(z, -z, 1/z, -1/z, z/z) // " 0 -0 +Inf -Inf NaN"

La funzione math.IsNaN verifica se il suo argomento è un valore non numerico e math.NaN restituisce tale
valore. È allettante usare NaN come valore di riferimento in un calcolo numerico, ma verificare se un
risultato specifico del calcolo è uguale a NaN è pericoloso, perché qualsiasi confronto con NaN dà
sempre esito negativo:
nan := math.NaN()
fmt.Println(nan == nan, nan < nan, nan > nan) // "falso falso falso"

Se una funzione che restituisce un risultato in virgola mobile potrebbe fallire, è meglio segnalare il
fallimento separatamente, come in questo caso:
func compute() (value float64, ok bool) {
// ...
se fallito {
restituire 0, falso
}
restituire il risultato, true
}

www.it-ebooks.info
58 CAPITOLO 3. TIPI DI DATI DI BASE

Il prossimo programma illustra il calcolo grafico in virgola mobile. Esso traccia una funzione di due
variabili z = f(x, y) come una superficie tridimensionale a maglia metallica, utilizzando Scalable
Vector Graphics (SVG), una notazione XML standard per i disegni a linee. La Figura 3.1 mostra un
esempio di output per la funzione sin(r)/r, dove r è sqrt(x*x+y*y).

Figura 3.1. Grafico della superficie della funzione sin(r)/r.

gopl.io/ch3/superficie
// Surface calcola un rendering SVG di una funzione di superficie 3D. pacchetto
main

importare (
"fmt"
"matematica"
)

const (
larghezza, altezza = 600, 320 // dimensione della tela in
pixel celle = 100 // numero di celle della
griglia
xyrange = 30.0 // intervalli degli assi (-
xyrange..+xyrange) xyscale = larghezza / 2 / xyrange // pixel per unità
xoy
scala z = altezza * 0,4 // pixel per unità z
angolo = math.Pi / 6 // angolo degli assi x e y (=30°)
)

var sin30, cos30 = math.Sin(angolo), math.Cos(angolo) // sin(30°), cos(30°)

www.it-ebooks.info
SEZIONE 3.2. NUMERI IN VIRGOLA MOBILE 59

func main() {
fmt.Printf("<svg xmlns='http://www.w3.org/2000/svg'"+ "style='stroke: grey;
fill: white; stroke-width: 0.7'"+ "width='%d' height='%d'>", width,
height)
per i := 0; i < celle; i++ { per j :=
0; j < celle; j++ {
ax, ay := angolo(i+1, j) bx,
by := angolo(i, j) cx, cy :=
angolo(i, j+1)
dx, dy := angolo(i+1, j+1)
fmt.Printf("<punti poligono='%g,%g,%g,%g,%g,%g,%g'/>n", ax, ay, bx, by,
cx, cy, dx, dy)
}
}
fmt.Println("</svg>")
}
func corner(i, j int) (float64, float64) {
// Trova il punto (x,y) nell'angolo della cella
(i,j). x := xyrange * (float64(i)/celle - 0,5)
y := xyrange * (float64(j)/celle - 0,5)
// Calcolo dell'altezza della
superficie z. z := f(x, y)
// Proietta (x,y,z) in modo isometrico sulla tela SVG 2-D (sx,sy). sx :=
width/2 + (x-y)*cos30*xyscale
sy := height/2 + (x+y)*sin30*xyscale - z*zscale return sx,
sy
}
func f(x, y float64) float64 {
r := math.Hypot(x, y) // distanza da (0,0) return
math.Sin(r) / r
}

Si noti che la funzione angolo restituisce due valori, le coordinate dell'angolo della cella.
La spiegazione del funzionamento del programma richiede solo una geometria di base, ma si può
tranquillamente sorvolare, dato che lo scopo è illustrare il calcolo in virgola mobile. L'essenza del
programma consiste nella mappatura tra tre diversi sistemi di coordinate, illustrati nella Figura 3.2. Il
primo è una griglia 2-D di 100×100 celle identificate da coordinate intere (i, j), a partire da (0, 0)
nell'angolo posteriore. Si traccia dal retro verso l'avanti, in modo che i poligoni di sfondo possano essere
oscurati da quelli in primo piano.

Il secondo sistema di coordinate è una maglia di coordinate tridimensionali in virgola mobile (x, y, z),
dove x e y sono funzioni lineari di i e j, traslate in modo che l'origine sia al centro e scalate dalla costante
xyrange. L'altezza z è il valore della funzione di superficie f (x, y).

Il terzo sistema di coordinate è il piano dell'immagine 2-D, con (0, 0) nell'angolo in alto a sinistra. I punti
in questo piano sono indicati con (sx, sy). Utilizziamo una proiezione isometrica per mappare ciascun
punto 3-D

www.it-ebooks.info
60 CAPITOLO 3. TIPI DI DATI DI BASE

Figura 3.2. Tre diversi sistemi di coordinate.

(x, y, z) sulla tela 2-D. Un punto appare più a destra sulla tela quanto maggiore è il suo valore x o quanto
minore è il suo valore y. Un punto appare più in basso sulla tela quanto maggiore è il suo valore x o y, e
quanto minore è il suo valore z. I fattori di scala verticale e orizzontale per x e y sono derivati dal seno e
dal coseno di un angolo di 30°. Il fattore di scala per z, 0,4, è un parametro arbitrario.

Per ogni cella della griglia 2-D, la funzione principale calcola le coordinate sulla tela immagine dei
quattro angoli del poligono ABCD, dove B corrisponde a (i, j) e A, C e D sono i suoi vicini, quindi
stampa un'istruzione SVG per disegnarlo.

Esercizio 3.1: Se la funzione f restituisce un valore float64 non finito, il file SVG conterrà elementi
<poligoni> non validi (anche se molti renderizzatori SVG gestiscono questo problema con grazia).
Modificare il programma per saltare i poligoni non validi.

Esercizio 3.2: Sperimentate la visualizzazione di altre funzioni del pacchetto matematico. Riuscite a
produrre una scatola di uova, delle gobbe o una sella?

Esercizio 3.3: Colorare ogni poligono in base alla sua altezza, in modo che i picchi siano colorati di rosso
(#ff0000) e le valli di blu (#0000ff).

Esercizio 3.4: Seguendo l'approccio dell'esempio di Lissajous nella Sezione 1.7, costruire un server web
che calcoli le superfici e scriva i dati SVG al client. Il server deve impostare l'intestazione Con- tent-Type
in questo modo:

w.Header().Set("Content-Type", "image/svg+xml")

(Questo passaggio non era necessario nell'esempio di Lissajous, perché il server utilizza un'euristica
standard per riconoscere formati comuni come PNG dai primi 512 byte della risposta e genera
l'intestazione corretta). Consentire al client di specificare valori come altezza, larghezza e colore come
parametri di richiesta HTTP.

www.it-ebooks.info
SEZIONE 3.3. NUMERI COMPLESSI 61

3.3. Numeri complessi

Go fornisce due dimensioni di numeri complessi, complex64 e complex128, i cui componenti sono
rispettivamente float32 e float64. La funzione integrata complex crea un numero complesso dalle
sue componenti reali e immaginarie e le funzioni integrate real e imag estraggono tali componenti:

var x complex128 = complex(1, 2) // 1+2i var y


complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y) // "(-5+10i)"
fmt.Println(real(x*y)) // "-5"
fmt.Println(imag(x*y)) // "10"

Se un letterale in virgola mobile o un letterale intero decimale è seguito immediatamente da i,


come 3,141592i o 2i, diventa un letterale immaginario, che indica un numero complesso con una
componente reale pari a zero:

fmt.Println(1i * 1i) // "(-1+0i)", i$ = -1

In base alle regole dell'aritmetica costante, le costanti complesse possono essere aggiunte ad altre costanti
(intere o in virgola mobile, reali o immaginarie), consentendoci di scrivere numeri complessi in modo
naturale, come 1+2i o, equivalentemente, 2i+1. Le dichiarazioni di x e y sopra riportate possono essere
semplificate:

x := 1 + 2i y
:= 3 + 4i

I numeri complessi possono essere confrontati per l'uguaglianza con == e !=. Due numeri complessi sono
uguali se le loro parti reali sono uguali e le loro parti immaginarie sono uguali.

Il pacchetto math/cmplx fornisce funzioni di libreria per lavorare con i numeri complessi, come la radice
quadrata complessa e le funzioni di esponenziazione.

fmt.Println(cmplx.Sqrt(-1)) // "(0+1i)"

Il programma seguente utilizza l'aritmetica complex128 per generare un insieme di Mandelbrot.

gopl.io/ch3/mandelbrot
// Mandelbrot emette un'immagine PNG del frattale di Mandelbrot.
pacchetto main

importare (
"image"
"image/color"
"image/png"
"math/cmplx"
"os"
)

www.it-ebooks.info
62 CAPITOLO 3. TIPI DI DATI DI BASE

func main() {
const (
xmin, ymin, xmax, ymax = -2, -2, +2, +2
larghezza, altezza = 1024, 1024
)

img := image.NewRGBA(image.Rect(0, 0, width, height)) for py :=


0; py < height; py++ {
y := float64(py)/altezza*(ymax-ymin) + ymin for px
:= 0; px < width; px++ {
x := float64(px)/larghezza*(xmax-xmin) + xmin z
:= complesso(x, y)
// Il punto immagine (px, py) rappresenta il valore complesso z.
img.Set(px, py, mandelbrot(z))
}
}
png.Encode(os.Stdout, img) // NOTA: ignorare gli errori
}

func mandelbrot(z complex128) color.Color { const


iterations = 200
const contrasto = 15

var v complesso128
per n := uint8(0); n < iterazioni; n++ { v =
v*v + z
se cmplx.Abs(v) > 2 {
restituire color.Gray{255 - contrasto*n}
}
}
restituire color.Black
}

I due cicli annidati iterano su ogni punto di un'immagine raster in scala di grigi 1024×1024 che
rappresenta la porzione da -2 a +2 del piano complesso. Il programma verifica se, ripetendo l'elevazione
al quadrato e l'addizione del numero che il punto rappresenta, alla fine si riesce a ''sfuggire'' al cerchio di
raggio 2. In caso affermativo, il punto viene ombreggiato per il numero di volte in cui viene
rappresentato. In caso affermativo, il punto viene ombreggiato dal numero di iterazioni che ha impiegato
per uscire. In caso contrario, il valore appartiene all'insieme di Mandelbrot e il punto rimane nero.
Infine, il programma scrive sul suo output standard l'immagine codificata in PNG del frattale iconico,
mostrata nella Figura 3.3.
Esercizio 3.5: Implementare un insieme di Mandelbrot a colori utilizzando la funzione
image.NewRGBA e il tipo color.RGBA o color.YCbCr.

Esercizio 3.6: Il sovracampionamento è una tecnica per ridurre l'effetto della pixelatura calcolando il
valore del colore in diversi punti all'interno di ciascun pixel e prendendone la media. Il metodo più
semplice consiste nel dividere ogni pixel in quattro ''sottopixel''. Implementarlo.
Esercizio 3.7: Un altro semplice frattale utilizza il metodo di Newton per trovare soluzioni complesse a
una funzione come z4-1 = 0. Colorate ogni punto di partenza con il numero di iterazioni necessarie per
avvicinarsi a una delle quattro radici. Colorare ogni punto in base alla radice a cui si avvicina.

www.it-ebooks.info
SEZIONE 3.4. BOOLEAN 63

Figura 3.3. L'insieme di Mandelbrot.

Esercizio 3.8: Il rendering dei frattali ad alti livelli di zoom richiede una grande precisione aritmetica.
Implementare lo stesso frattale usando quattro diverse rappresentazioni di numeri: complex64, com-
plex128, big.Float e big.Rat. (Questi ultimi due tipi si trovano nel pacchetto math/big. Float utilizza
numeri in virgola mobile arbitrari ma a precisione limitata; Rat utilizza numeri razionali a precisione non
limitata). Come si comportano in termini di prestazioni e di utilizzo della memoria? A quali livelli di
zoom diventano visibili gli artefatti di rendering?

Esercizio 3.9: Scrivere un server Web che esegua il rendering dei frattali e scriva i dati dell'immagine al
client. Consentire al client di specificare i valori di x, y e zoom come parametri della richiesta HTTP.

3.4. Booleani

Un valore di tipo bool, o booleano, ha solo due valori possibili, true e false. Le condizioni nelle
istruzioni if e for sono booleane e gli operatori di confronto come == e < producono un risultato
booleano. L'operatore unario ! è la negazione logica, quindi !true è falso, oppure, si potrebbe
dire, (!true==false)==true, anche se, p e r una questione di stile, semplifichiamo sempre le
espressioni booleane ridondanti come x==true a x.

I valori booleani possono essere combinati con gli operatori && (AND) e || (OR), che hanno un
comportamento a corto circuito: se la risposta è già determinata dal valore dell'operando di sinistra,
l'operando di destra non viene valutato, rendendo sicura la scrittura di espressioni come questa:
s !"= "" && s[0] == 'x'

dove s[0] andrebbe in panico se applicato a una stringa vuota.

Poiché && ha una precedenza maggiore di || (mnemonico: && è la moltiplicazione booleana, || è


l'addizione booleana), non sono necessarie parentesi per le condizioni di questa forma:

www.it-ebooks.info
64 CAPITOLO 3. TIPI DI DATI DI BASE

if 'a' <= c && c <= 'z' || 'A' <=


c && c <= 'Z' || '0' <= c &&
c <= '9' {
// ...lettera o cifra ASCII...
}

Non esiste una conversione implicita da un valore booleano a un valore numerico come 0 o 1, o viceversa. È
necessario utilizzare un if esplicito, come in
i := 0
se b {
i = 1
}

Potrebbe valere la pena scrivere una funzione di conversione se questa operazione fosse necessaria spesso:
// btoi restituisce 1 se b è vero e 0 se è falso.
func btoi(b bool) int {
se b {
ritorno 1
}
ritorno 0
}

L'operazione inversa è talmente semplice da non giustificare una funzione, ma per simmetria eccola qui:
// itob segnala se i è diverso da zero. func
itob(i int) bool { return i != 0 }

3.5. Corde

Una stringa è una sequenza immutabile di byte. Le stringhe possono contenere dati arbitrari, compresi i
byte con valore 0, ma di solito contengono testo leggibile dall'uomo. Le stringhe di testo sono
convenzionalmente interpretate come sequenze codificate UTF-8 di punti di codice Unicode (rune), che
analizzeremo in dettaglio tra poco.
La funzione built-in len restituisce il numero di byte (non di rune) in una stringa e l'indice
L'operazione s[i] recupera l'i-esimo byte della stringa s, dove 0 ≤ i < len(s).
s := "ciao, mondo"
fmt.Println(len(s)) // "12"
fmt.Println(s[0], s[7]) // "104 119" ( 'h' e 'w')

Il tentativo di accedere a un byte al di fuori di questo intervallo provoca un panico:


c := s[len(s)] // panico: indice fuori dall'intervallo

L'i-esimo byte di una stringa non è necessariamente l'i-esimo carattere di una stringa, perché la codifica
UTF-8 di un punto di codice non ASCII richiede due o più byte. Il lavoro con i caratteri verrà discusso a
breve.

www.it-ebooks.info
SEZIONE 3.5. STRINGHE 65

L'operazione di sottostringa s[i:j] produce una nuova stringa composta dai byte della stringa originale a
partire dall'indice i e fino al byte dell'indice j, escluso. Il risultato contiene j-i byte.
fmt.Println(s[0:5]) // "ciao"

Anche in questo caso, si verifica un panico se uno dei due indici è fuori dai limiti o se j è inferiore a i.
Uno o entrambi gli operandi i e j possono essere omessi, nel qual caso i valori predefiniti di 0
(l'inizio della stringa) e len(s) (la sua fine).
fmt.Println(s[:5]) // "ciao" fmt.Println(s[7:]) //
"mondo" fmt.Println(s[:]) // "ciao, mondo"

L'operatore + crea una nuova stringa concatenando due stringhe:


fmt.Println("goodbye" + s[5:]) // "addio, mondo"

Le stringhe possono essere confrontate con operatori di confronto come == e <; il confronto viene fatto
byte per byte, quindi il risultato è l'ordinamento lessicografico naturale.
I valori delle stringhe sono immutabili: la sequenza di byte contenuta i n un valore di stringa non può
mai essere modificata, anche se ovviamente è possibile assegnare un nuovo valore a una variabile
stringa. Per aggiungere una stringa a un'altra, ad esempio, possiamo scrivere
s := "piede sinistro"
t := s
s += ", piede destro"

Questo non modifica la stringa originariamente contenuta in s, ma fa sì che s contenga la nuova stringa
formata dall'istruzione +=; nel frattempo, t contiene ancora la vecchia stringa.
fmt.Println(s) // "piede sinistro, piede destro"
fmt.Println(t) // "piede sinistro"

Poiché le stringhe sono immutabili, non sono consentiti costrutti che tentino di modificare i dati di una
stringa sul posto:
s[0] = 'L' // errore di compilazione: impossibile assegnare a s[0]

L'immutabilità significa che è sicuro che due copie di una stringa condividano la stessa memoria
sottostante, rendendo economica la copia di stringhe di qualsiasi lunghezza. Allo stesso modo, una
stringa s e una sottostringa come s[7:] possono condividere in modo sicuro gli stessi dati, quindi anche
l'operazione di sottostringa è economica. In entrambi i casi non viene allocata nuova memoria. La Figura
3.4 illustra la disposizione di una stringa e di due sue sottostringhe che condividono lo stesso array di
byte sottostante.

3.5.1. Letterali di stringa

Un valore di stringa può essere scritto come letterale di stringa, una sequenza di byte racchiusa tra doppi apici:
"Ciao, B$"

www.it-ebooks.info
66 CAPITOLO 3. TIPI DI DATI DI BASE

Figura 3.4. La stringa "hello, world" e due sottostringhe.

Poiché i file sorgente di Go sono sempre codificati in UTF-8 e le stringhe di testo di Go sono
convenzionalmente interpretate come UTF-8, è possibile includere i punti di codice Unicode nei letterali
di stringa.
All'interno di una stringa letterale con doppio apice, le sequenze di escape che iniziano con un backslash
\ possono essere utilizzate per inserire valori di byte arbitrari nella stringa. Una serie di escape gestisce i
codici di controllo ASCII come newline, ritorno a capo e tabulazione:
\a ''allarme'' o campanello
\b backspace
\f alimentazione della forma
\n newline
\r ritorno a capo
\t scheda
\v scheda verticale
\' virgolette singole (solo nel letterale runico '\')
\" doppio apice (solo all'interno di letterali "...")
\\ backslash
I byte arbitrari possono anche essere inclusi in stringhe letterali utilizzando escape esadecimali o ottali.
Un escape esadecimale si scrive \xhh, con esattamente due cifre esadecimali h (in maiuscolo o
minuscolo). Un escape ottale è scritto \ooo con esattamente tre cifre ottali o (da 0 a 7) non superiori a
\377. Entrambi indicano un singolo byte con il valore specificato. Più avanti vedremo come codificare
numericamente i punti di codice Unicode nei letterali di stringa.
Un letterale di stringa grezzo si scrive `...`, usando i backquote al posto dei doppi apici. All'interno di
un letterale di stringa grezzo, non vengono elaborate sequenze di escape; il contenuto viene preso alla
lettera, compresi i backslash e le newline, quindi un letterale di stringa grezzo può estendersi su più righe
del sorgente del programma. L'unica elaborazione consiste nell'eliminazione dei ritorni a capo, in modo
che il valore della stringa sia lo stesso su tutte le piattaforme, comprese quelle che convenzionalmente
inseriscono i ritorni a capo nei file di testo.
I letterali di stringa grezzi sono un modo comodo per scrivere espressioni regolari, che tendono ad avere
molti backslash. Sono utili anche per i modelli HTML, i letterali JSON, i messaggi di utilizzo dei
comandi e simili, che spesso si estendono su più righe.

www.it-ebooks.info
SEZIONE 3.5. STRINGHE 67

const GoUsage = `Go è uno strumento per la gestione del codice sorgente Go.

Utilizzo:
comando go [argomenti]
...`

3.5.2. Unicode

Molto tempo fa, la vita era semplice e c'era, almeno in una visione campanilistica, un solo set di caratteri
da gestire: ASCII, l'American Standard Code for Information Interchange. L'ASCII, o più precisamente
l'US-ASCII, utilizza 7 bit per rappresentare 128 ''caratteri'': le lettere maiuscole e minuscole dell'inglese,
le cifre e una serie di caratteri di punteggiatura e di controllo dei dispositivi. Per gran parte dei primi
tempi dell'informatica, questa soluzione era adeguata, ma lasciava una frazione molto ampia della
popolazione mondiale nell'impossibilità di utilizzare i propri sistemi di scrittura nei computer. Con la
crescita di Internet, i dati in una miriade di lingue sono diventati molto più comuni. Come si può gestire
questa ricca varietà e, se possibile, in modo efficiente?
La risposta è Unicode (unicode.org), che raccoglie tutti i caratteri di tutti i sistemi di scrittura del mondo,
più accenti e altri segni diacritici, codici di controllo come tabulazione e ritorno a capo e molte altre cose
esoteriche, e assegna a ciascuno di essi un numero standard chiamato punto di codice Unicode o, nella
terminologia Go, una runa.
La versione 8 di Unicode definisce i punti di codice per oltre 120.000 caratteri in ben 100 lingue e
scritture. Come vengono rappresentati nei programmi e nei dati del computer? Il tipo di dati
naturale per contenere una singola runa è int32, ed è quello che Go usa; ha il sinonimo di runa
proprio per questo scopo.
Potremmo rappresentare una sequenza di rune come una sequenza di valori int32. In questa
rappresentazione, chiamata UTF-32 o UCS-4, la codifica di ogni punto di codice Unicode ha la stessa
dimensione, 32 bit. Si tratta di una rappresentazione semplice e uniforme, ma che utilizza molto più
spazio del necessario, dato che la maggior parte del testo leggibile al computer è in formato ASCII, che
richiede solo 8 bit o 1 byte per carattere. Tutti i caratteri di uso comune sono ancora meno di 65.536, che
potrebbero stare in 16 bit. Possiamo fare di meglio?

3.5.3. UTF-8

UTF-8 è una codifica a lunghezza variabile dei punti di codice Unicode come byte. UTF-8 è stato
inventato da Ken Thompson e Rob Pike, due dei creatori di Go, ed è ora uno standard Unicode. Utilizza
da 1 a 4 byte per rappresentare ogni runa, ma solo 1 byte per i caratteri ASCII e solo 2 o 3 byte per la
maggior parte delle rune di uso comune. I bit di ordine superiore del primo byte della codifica di una
runa indicano il numero di byte successivi. Uno 0 di ordine superiore indica un ASCII a 7 bit, in cui ogni
runa occupa solo 1 byte, quindi è identico all'ASCII convenzionale. Un ordine alto 110 indica che la runa
occupa 2 byte; il secondo byte inizia con 10. Le rune più grandi hanno una codifica analoga.

www.it-ebooks.info
68 CAPITOLO 3. TIPI DI DATI DI BASE

0xxxxxx rune 0-127 (ASCII)


11xxxxx 10xxxxxx 128-2047 (valori <128 non
utilizzati)
110xxxx 10xxxx 10xxxx 2048-65535 (valori <2048 non
utilizzati)
1110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536-0x10ffff (altri valori non utilizzati)

Una codifica a lunghezza variabile preclude l'indicizzazione diretta per accedere al carattere n-esimo di
una stringa, ma UTF-8 ha molte proprietà desiderabili per compensare. La codifica è compatta,
compatibile con ASCII e autosincronizzante: è possibile trovare l'inizio di un carattere arretrando di non
più di tre byte. È anche un codice a prefisso, quindi può essere decodificato da sinistra a destra senza
alcuna ambiguità o anticipo. Nessuna codifica di una runa è una sottostringa di un'altra, o addirittura di
una sequenza di altre, quindi è possibile cercare una runa semplicemente cercando i suoi byte, senza
preoccuparsi del contesto precedente. L'ordine lessicografico dei byte corrisponde all'ordine dei punti di
codice Uni- code, quindi l'ordinamento UTF-8 funziona in modo naturale. Non ci sono byte NUL (zero)
incorporati, il che è comodo per i linguaggi di programmazione che usano il NUL per terminare le
stringhe.

I file sorgenti di Go sono sempre codificati in UTF-8 e UTF-8 è la codifica preferita per le stringhe
di testo manipolate dai programmi Go. Il pacchetto unicode fornisce funzioni per lavorare con le
singole rune (come distinguere le lettere dai numeri o convertire una lettera maiuscola in una
minuscola) e il pacchetto unicode/utf8 fornisce funzioni per codificare e decodificare le rune come
byte utilizzando UTF-8.

Molti caratteri Unicode sono difficili da digitare su una tastiera o da distinguere visivamente da altri di
aspetto simile; alcuni sono addirittura invisibili. Gli escape Unicode nei letterali di stringa di Go ci
permettono di specificarli con il loro valore numerico di punto di codice. Esistono due forme, \uhhhh
per un valore a 16 bit e \Uhhhhhhh per un valore a 32 bit, dove ogni h è una cifra esadecimale; la
necessità della forma a 32 bit si presenta molto raramente. Ciascuna indica la codifica UTF-8 del punto
di codice specificato. Ad esempio, i seguenti letterali di stringa rappresentano tutti la stessa stringa di sei
byte:
"B$"
"\xe4\xb8\x96\xe7\x95\x8c"
"\u4e16\u754c"
"\U00004e16\U0000754c"

Le tre sequenze di escape sopra riportate forniscono notazioni alternative per la prima stringa, ma i
valori che denotano sono identici.

Gli escape Unicode possono essere utilizzati anche nei letterali delle rune. Questi tre letterali sono equivalenti:
'B' '\u4e16' ' \ U 0 0 0 0 4 e 1 6 '

Una runa il cui valore è inferiore a 256 può essere scritta con un singolo escape esadecimale, come '\x41'
per 'A', ma per valori superiori è necessario utilizzare un escape \u o \U. Di conseguenza,
'\xe4\xb8\x96' non è un letterale runico legale, anche se questi tre byte sono una codifica UTF-8
valida di un singolo punto di codice.

Grazie alle proprietà di UTF-8, molte operazioni sulle stringhe non richiedono la decodifica. Possiamo
verificare se una stringa ne contiene un'altra come prefisso:

www.it-ebooks.info
SEZIONE 3.5. STRINGHE 69

func HasPrefix(s, prefix string) bool {


return len(s) >= len(prefisso) && s[:len(prefisso)] == prefisso
}

o come suffisso:
func HasSuffix(s, suffisso stringa) bool {
return len(s) >= len(suffisso) && s[len(s)-len(suffisso):] == suffisso
}

o come sottostringa:
func Contains(s, substr string) bool { for i
:= 0; i < len(s); i++ {
se HasPrefix(s[i:], substr) {
restituisce true
}
}
restituire false
}

utilizzando la stessa logica per il testo codificato in UTF-8 e per i byte grezzi. Questo non è vero per
le altre codifiche. (Le funzioni di cui sopra sono tratte dal pacchetto strings, sebbene la sua
implementazione di Contains utilizzi una tecnica di hashing per effettuare ricerche più efficienti).
D'altra parte, se ci interessano davvero i singoli caratteri Unicode, dobbiamo usare altri meccanismi.
Consideriamo la stringa del nostro primo esempio, che include due caratteri dell'Asia orientale. La
Figura 3.5 ne illustra la rappresentazione in memoria. La stringa contiene 13 byte, ma interpretata come
UTF-8, codifica solo nove punti di codice o rune:

importare "unicode/utf8"

s := "Ciao, B$"
fmt.Println(len(s)) // "13"
fmt.Println(utf8.RuneCountInString(s)) // "9"

Per elaborare questi caratteri, abbiamo bisogno di un decodificatore UTF-8. Il pacchetto


unicode/utf8 ne fornisce uno che si può usare in questo modo:
per i := 0; i < len(s); {
r, size := utf8.DecodeRuneInString(s[i:])
fmt.Printf("%d\t%c\n", i, r)
i += dimensione
}

Ogni chiamata a DecodeRuneInString restituisce r, la runa stessa, e size, il numero di byte occupati
dalla codifica UTF-8 di r. L a size viene usata per aggiornare l'indice di byte i della runa successiva nella
stringa. Ma questa procedura è goffa e abbiamo sempre bisogno di cicli di questo tipo. Fortunatamente,
il ciclo range di Go, se applicato a una stringa, esegue la decodifica UTF-8 in modo implicito. L'output
del ciclo sottostante è mostrato anche nella Figura 3.5; si noti come l'indice salti di più di 1 per ogni
runa non ASCII.

www.it-ebooks.info
70 CAPITOLO 3. TIPI DI DATI DI BASE

Figura 3.5. Un ciclo di range decodifica una stringa codificata UTF-8.

per i, r := intervallo "Ciao, B$" {


fmt.Printf("%d\t%q%d\n", i, r, r)
}

Si può usare un semplice ciclo di intervallo per contare il numero di rune in una stringa, come questo:
n := 0
per _, _ = intervallo s {
n++
}

Come per le altre forme di range loop, possiamo omettere le variabili che non ci servono:
n := 0
per l'intervallo s
{ n++
}

Oppure possiamo semplicemente chiamare utf8.RuneCountInString(s).


Abbiamo accennato in precedenza al fatto che per convenzione in Go le stringhe di testo vengono inter-
prete come sequenze di punti di codice Unicode codificati in UTF-8, ma per un uso corretto dei cicli di
range sulle stringhe, non si tratta solo di una convenzione, ma di una necessità. Cosa succede se facciamo
un range su una stringa contenente dati binari arbitrari o, se vogliamo, dati UTF-8 contenenti errori?
Ogni volta che un decodificatore UTF-8, sia esso esplicito in una chiamata a utf8.DecodeRuneInString
o implicito in un ciclo di range, consuma un byte di input inatteso, genera uno speciale carattere
sostitutivo Unicode, '\uFFFD', che di solito viene stampato come un punto interrogativo bianco
all'interno di un esagono nero o di una forma simile a un diamante d. Quando un programma
incontra questo valore di runa, spesso è un segno che qualche parte a monte del sistema che ha
generato i dati della stringa è stato

www.it-ebooks.info
SEZIONE 3.5. STRINGHE 71

noncurante nel trattamento delle codifiche di testo.


UTF-8 è eccezionalmente comodo come formato di interscambio, ma all'interno di un programma le
rune possono essere più comode perché hanno dimensioni uniformi e sono quindi facilmente
indicizzabili in array e slices.
Una conversione []runa applicata a una stringa codificata in UTF-8 restituisce la sequenza di punti
di codice Unicode che la stringa codifica:
// "programma" in katakana giapponese
s := ">◻Ӳ@L"
fmt.Printf("% x\n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0" r := []rune(s)
fmt.Printf("%x\n", r) // "[30d7 30ed 30b0 30e9 30e0]"

(Il verbo % x nella prima stampa inserisce uno spazio tra ogni coppia di cifre esadecimali).
Se una fetta di rune viene convertita in una stringa, si ottiene la concatenazione delle codifiche UTF-8 di
ciascuna runa:
fmt.Println(string(r)) // ">◻Ӳ@L"

La conversione di un valore intero in una stringa interpreta l'intero come valore di runa e fornisce la
rappresentazione UTF-8 di tale runa:
fmt.Println(stringa(65)) // "A", non "65"
fmt.Println(string(0x4eac)) // "C"

Se la runa non è valida, viene sostituito il carattere sostitutivo:


fmt.Println(stringa(1234567)) // "d"

3.5.4. Stringhe e fette di byte

Quattro pacchetti standard sono particolarmente importanti per la manipolazione delle stringhe:
bytes, strings, strconv e unicode. Il pacchetto strings fornisce molte funzioni per cercare, sostituire,
confrontare, tagliare, dividere e unire le stringhe.
Il pacchetto bytes ha funzioni simili per manipolare fette di byte, di tipo []byte, che condividono alcune
proprietà con le stringhe. Poiché le stringhe sono immutabili, la creazione di stringhe in modo
incrementale può comportare molte allocazioni e copie. In questi casi, è più efficiente usare il tipo
bytes.Buffer, che mostreremo tra poco.

Il pacchetto strconv fornisce funzioni per la conversione di valori booleani, interi e in virgola mobile
da e verso le loro rappresentazioni in stringa e funzioni per il quoting e l'unquoting delle stringhe.
Il pacchetto unicode fornisce funzioni come IsDigit, IsLetter, IsUpper e IsLower per classificare le
rune. Ogni funzione accetta un singolo argomento di runa e restituisce un booleano. Le funzioni di
conversione, come ToUpper e ToLower, convertono una runa nel caso dato, se si tratta di una lettera. Tutte
queste funzioni utilizzano le categorie standard Unicode per lettere, cifre e così via. Le stringhe

www.it-ebooks.info
72 CAPITOLO 3. TIPI DI DATI DI BASE

ha funzioni simili, chiamate anche ToUpper e ToLower, che restituiscono una nuova stringa con la
trasformazione specificata applicata a ogni carattere della stringa originale.
La funzione basename che segue è stata ispirata dall'omonima utility della shell Unix. Nella nostra
versione, basename(s) rimuove qualsiasi prefisso di s che assomigli a un percorso di file system
con i componenti separati da barre, e rimuove qualsiasi suffisso che assomigli a un tipo di file:
fmt.Println(basename("a/b/c.go")) // "c"
fmt.Println(basename("c.d.go")) // "c.d"
fmt.Println(basename("abc")) // "abc"

La prima versione di basename svolge tutto il lavoro senza l'aiuto delle librerie:
gopl.io/ch3/basename1
// il nome di base rimuove i componenti della directory e il suffisso .suffisso.
// ad esempio, a => a, a.go => a, a/b/c.go => c, a/b.c.go => b.c func
basename(s string) string {
// Scartare l'ultimo '/' e tutto ciò che lo precede.
for i := len(s) - 1; i >= 0; i-- {
se s[i] == '/' { s
= s[i+1:]
break
}
}
// Conserva tutto ciò che precede l'ultimo '.'.
for i := len(s) - 1; i >= 0; i-- {
se s[i] == '.' { s
= s[:i] break
}
}
restituire s
}

Una versione più semplice utilizza la funzione di libreria strings.LastIndex:


gopl.io/ch3/basename2
func basename(s string) string {
slash := strings.LastIndex(s, "/") // -1 se "/" non viene trovato s
= s[slash+1:]
if dot := strings.LastIndex(s, "."); dot >= 0 { s =
s[:dot]
}
restituire s
}

I pacchetti path e path/filepath forniscono un insieme più generale di funzioni per manipolare i nomi
gerarchici. Il pacchetto path funziona con percorsi delimitati da slash su qualsiasi piattaforma. Non
dovrebbe essere usato per i nomi di file, ma è appropriato per altri domini, come la componente
percorso di un URL. Al contrario, path/filepath manipola i nomi di file utilizzando le regole della
piattaforma host, come ad esempio /foo/bar per POSIX o c:\foo\bar su Microsoft Windows.

www.it-ebooks.info
SEZIONE 3.5. STRINGHE 73

Continuiamo con un altro esempio di sottostringa. Si tratta di prendere una rappresentazione in stringa
di un numero intero, come "12345", e di inserire delle virgole ogni tre punti, come in "12.345". Questa
versione funziona solo per i numeri interi; la gestione dei numeri in virgola mobile è lasciata come
esercizio.

gopl.io/ch3/comma
// virgola inserisce delle virgole in una stringa di numeri interi decimali non
negativi. func comma(s string) string {
n := len(s) if
n <= 3 {
restituire s
}
return comma(s[:n-3]) + "," + s[n-3:]
}

L'argomento della virgola è una stringa. Se la sua lunghezza è minore o uguale a 3, non è necessaria la
virgola. In caso contrario, la virgola si richiama ricorsivamente con una sottostringa composta da tutti i
caratteri tranne gli ultimi tre e aggiunge una virgola e gli ultimi tre caratteri al risultato della chiamata
ricorsiva.

Una stringa contiene un array di byte che, una volta creato, è immutabile. Al contrario, gli elementi di
una slice di byte possono essere modificati liberamente.

Le stringhe possono essere convertite in fette di byte e viceversa:

s := "abc"
b := []byte(s) s2
:= stringa(b)

Concettualmente, la conversione []byte(s) alloca un nuovo array di byte contenente una copia dei byte
di s e produce una slice che fa riferimento all'intero array. Un compilatore ottimizzato può essere in
grado di evitare l'allocazione e la copia in alcuni casi, ma in generale la copia è necessaria per garantire
che i byte di s rimangano invariati anche se quelli di b vengono successivamente modificati. Anche la
conversione da byte slice a stringa con string(b) effettua una copia, per garantire l'immutabilità della
stringa s2 risultante.

Per evitare conversioni e inutili allocazioni di memoria, molte delle funzioni di utilità del pacchetto
bytes sono direttamente parallele alle loro controparti del pacchetto strings. Ad esempio, ecco una
mezza dozzina di funzioni del pacchetto strings:

func Contains(s, substr string) bool func


Count(s, sep string) int
func Campi(s stringa) []stringa
func HasPrefix(s, prefix string) bool func
Index(s, sep string) int
func Join(a []stringa, sep stringa) stringa

e quelli corrispondenti dei byte:

www.it-ebooks.info
74 CAPITOLO 3. TIPI DI DATI DI BASE

func Contains(b, subslice []byte) bool func


Count(s, sep []byte) int
func Fields(s []byte) [][]byte
func HasPrefix(s, prefix []byte) bool func
Index(s, sep []byte) int
func Join(s [][]byte, sep []byte) []byte

L'unica differenza è che le stringhe sono state sostituite da fette di byte.

Il pacchetto bytes fornisce il tipo Buffer per una manipolazione efficiente delle fette di byte. Un
Buffer inizia vuoto, ma cresce man mano che vi vengono scritti dati di tipo stringa, byte e []byte.
Come mostra l'esempio seguente, una variabile bytes.Buffer non richiede inizializzazione perché il
suo valore zero è utilizzabile:
gopl.io/ch3/stampe
// intsToString è come fmt.Sprintf(values) ma aggiunge le virgole.
func intsToString(values []int) string {
var buf bytes.Buffer buf.WriteByte('[')
per i, v := intervallo di valori
{ se i > 0 {
buf.WriteString(", ")
}
fmt.Fprintf(&buf, "%d", v)
}
buf.WriteByte(']')
return buf.String()
}

func main() {
fmt.Println(intsToString([]int{1, 2, 3})) // "[1, 2, 3]"
}

Quando si aggiunge la codifica UTF-8 di una runa arbitraria a un bytes.Buffer, è meglio usare il metodo
WriteRune di bytes.Buffer, ma WriteByte va bene per caratteri ASCII come '[' e ']'.

Il tipo bytes.Buffer è estremamente versatile e, quando discuteremo delle interfacce nel Capitolo 7,
vedremo come può essere utilizzato in sostituzione di un file ogni volta che una funzione di I/O
necessita di un contenitore di byte (io.Writer), come fa Fprintf, o di una fonte di byte (io.Reader).

Esercizio 3.10: Scrivere una versione non ricorsiva di virgola, utilizzando bytes.Buffer invece della
concatenazione di stringhe.

Esercizio 3.11: Migliorare la virgola in modo che tratti correttamente i numeri in virgola mobile e un
segno opzionale.

Esercizio 3.12: Scrivere una funzione che indichi se due stringhe sono anagrammate tra loro, cioè
contengono le stesse lettere in ordine diverso.

www.it-ebooks.info
SEZIONE 3.6. COSTANTI 75

3.5.5. Conversioni tra stringhe e numeri

Oltre alle conversioni tra stringhe, rune e byte, è spesso necessario convertire tra valori numerici e le
loro rappresentazioni in stringa. Questo viene fatto con le funzioni del pacchetto strconv.
Per convertire un intero in una stringa, un'opzione è usare fmt.Sprintf; un'altra è usare la funzione
strconv.Itoa (''integer to ASCII''):
x := 123
y := fmt.Sprintf("%d", x)
fmt.Println(y, strconv.Itoa(x)) // " 123 123"

FormatInt e FormatUint possono essere utilizzati per formattare i numeri in una base diversa:
fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011"

I verbi fmt.Printf %b, %d, %u e %x sono spesso più convenienti delle funzioni Format, soprattutto se si
vogliono includere informazioni aggiuntive oltre al numero:
s := fmt.Sprintf("x=%b", x) // "x=1111011"

Per analizzare una stringa che rappresenta un numero intero, utilizzare le funzioni strconv Atoi o
ParseInt, oppure
ParseUint per i numeri interi senza segno:
x, err := strconv.Atoi("123") // x è un int
y, err := strconv.ParseInt("123", 10, 64) // base 10, fino a 64 bit

Il terzo argomento di ParseInt indica la dimensione del tipo di intero in cui deve rientrare il
risultato; ad esempio, 16 implica int16 e il valore speciale 0 implica int. In ogni caso, il tipo del
risultato y è sempre int64, che può essere convertito in un tipo più piccolo.
A volte fmt.Scanf è utile per analizzare input che consistono in miscele ordinate di stringhe e numeri su
una singola riga, ma può essere poco flessibile, soprattutto quando si tratta di input incompleti o
irregolari.

3.6. Costanti

Le costanti sono espressioni il cui valore è noto al compilatore e la cui valutazione è garantita a tempo di
compilazione, non a tempo di esecuzione. Il tipo sottostante di ogni costante è un tipo base: booleano,
stringa o numero.
Una dichiarazione const definisce valori nominati che hanno l'aspetto sintattico di variabili, ma il
cui valore è costante, il che impedisce modifiche accidentali (o nefaste) durante l'esecuzione del
programma. Ad esempio, una costante è più appropriata di una variabile per una costante matematica
come il pi greco, poiché il suo valore non cambia:
const pi = 3,14159 // approssimativamente; math.Pi è un'approssimazione migliore

Come per le variabili, una sequenza di costanti può comparire in un'unica dichiarazione; ciò sarebbe
appropriato per un gruppo di valori correlati:

www.it-ebooks.info
76 CAPITOLO 3. TIPI DI DATI DI BASE

const (
e = 2.71828182845904523536028747135266249775724709369995957496696763
pi = 3.14159265358979323846264338327950288419716939937510582097494459
)

Molti calcoli su costanti possono essere valutati completamente in fase di compilazione, riducendo il
lavoro necessario in fase di esecuzione e consentendo altre ottimizzazioni del compilatore. Gli errori
normalmente rilevati in fase di esecuzione possono essere segnalati in fase di compilazione quando i loro
operandi sono costanti, come la divisione di interi per zero, l'indicizzazione di stringhe fuori dai limiti e
qualsiasi operazione in virgola mobile che dia come risultato un valore non finito.

I risultati di tutte le operazioni aritmetiche, logiche e di confronto applicate a operandi costanti sono
essi stessi costanti, così come i risultati delle conversioni e delle chiamate ad alcune funzioni
incorporate come len, cap, real, imag, complex e unsafe.Sizeof (§13.1).

Poiché i loro valori sono noti al compilatore, le espressioni costanti possono comparire nei tipi, in
particolare come lunghezza di un tipo di array:
const IPv4Len = 4

// parseIPv4 analizza un indirizzo IPv4 (d.d.d.d). func


parseIPv4(s string) IP {
var p [IPv4Len]byte
// ...
}

Una dichiarazione di costante può specificare un tipo e un valore, ma in assenza di un tipo esplicito,
il tipo viene dedotto dall'espressione sul lato destro. Nel seguito, time.Duration è un tipo nominato il
cui tipo sottostante è int64 e time.Minute è una costante di quel tipo. Entrambe le costanti dichiarate
di seguito hanno quindi anche il tipo time.Duration, come rivelato da %T:
const noDelay time.Duration = 0 const
timeout = 5 * time.Minute
fmt.Printf("%T %[1]v\n", noDelay) // "time.Duration 0"
fmt.Printf("%T %[1]v\n", timeout) // "time.Duration 5m0s
fmt.Printf("%T % [1]v\n", time.Minute) // " time.Duration 1m0s"

Quando una sequenza di costanti viene dichiarata come gruppo, l'espressione di destra può essere
omessa per tutte le costanti tranne la prima del gruppo, il che implica che l'espressione precedente e il
suo tipo devono essere utilizzati di nuovo. Ad esempio:
const (
a = 1
b
c = 2
d
)

fmt.Println(a, b, c, d) // "1 1 2 2"

www.it-ebooks.info
SEZIONE 3.6. COSTANTI 77

Questo non è molto utile se l'espressione implicitamente copiata sul lato destro valuta sempre la stessa
cosa. Ma cosa succede se può variare? Questo ci porta a iota.

3.6.1. Il generatore costante iota

Una dichiarazione const può utilizzare il generatore di costanti iota, che viene utilizzato per creare una
sequenza di valori correlati senza indicare esplicitamente ciascuno di essi. In una dichiarazione const, il
valore di iota inizia con zero e aumenta di uno per ogni elemento della sequenza.
Ecco un esempio dal pacchetto time, che definisce costanti di tipo Weekday per i giorni della settimana, a
partire da zero per la domenica. Tipi di questo tipo sono spesso chiamati enu- merazioni, o enum in
breve.
tipo Giorno feriale

int const (
Domenica giorno feriale = iota
Lunedì
Martedì
Mercoledì
Giovedì
Venerdì
Sabato
)

Questo dichiara che la domenica è 0, il lunedì è 1 e così via.


Possiamo usare iota anche in espressioni più complesse, come in questo esempio tratto dal pacchetto net,
in cui a ciascuno dei 5 bit più bassi di un intero senza segno viene dato un nome distinto e
un'interpretazione booleana:
tipo Flags uint

const (
FlagUp Flags = 1 << iota // è attivo
FlagBroadcast // supporta la capacità di accesso broadcast
FlagLoopback // è un'interfaccia di loopback
FlagPointToPoint // appartiene a un collegamento punto-
punto FlagMulticast // supporta la capacità di accesso multicast
)

All'aumentare di iota, a ogni costante viene assegnato il valore di 1 << iota, che si valuta come potenze
successive di due, ciascuna corrispondente a un singolo bit. Possiamo usare queste costanti all'interno di
funzioni che testano, impostano o cancellano uno o più di questi bit:
gopl.io/ch3/netflag
func IsUp(v Flags) bool { return v&FlagUp == FlagUp } func
TurnDown(v *Flags) { *v &^= FlagUp }
func SetBroadcast(v *Flags) { *v |= FlagBroadcast }
func IsCast(v Flags) bool { return v&(FlagBroadcast|FlagMulticast) != 0 }

www.it-ebooks.info
78 CAPITOLO 3. TIPI DI DATI DI BASE

func main() {
var v Flags = FlagMulticast | FlagUp fmt.Printf("%b %t\n",
v, IsUp(v)) // "10001 vero" TurnDown(&v)
fmt.Printf("%b %t\n", v, IsUp(v)) // "10000 falso"
SetBroadcast(&v)
fmt.Printf("%b %t\n", v, IsUp(v)) // "10010 falso"
fmt.Printf("%b %t\n", v, IsCast(v)) // "10010 vero"
}

Come esempio più complesso di iota, questa dichiarazione nomina le potenze di 1024:
const (
_ = 1 << (10 * iota) KiB
// 1024
MiB // 1048576
GiB // 1073741824
TiB // 1099511627776 (supera 1 << 32)
PiB // 1125899906842624
EiB // 1152921504606846976
ZiB // 1180591620717411303424 (supera 1 << 64)
YiB // 1208925819614629174706176
)

Il meccanismo dello iota ha i suoi limiti. Ad esempio, non è possibile generare le potenze più familiari di
1000 (KB, MB e così via) perché non esiste un operatore di esponenziazione.
Esercizio 3.13: Scrivere le dichiarazioni di const per KB, MB e YB nel modo più compatto possibile.

3.6.2. Costanti non tipizzate

Le costanti in Go sono un po' insolite. Sebbene una costante possa avere qualsiasi tipo di dati di base
come int o float64, compresi i tipi di base denominati come time.Duration, molte costanti non
sono vincolate a un tipo particolare. Il compilatore rappresenta queste costanti non impegnate con una
precisione numerica molto maggiore rispetto ai valori dei tipi base e l'aritmetica su di esse è più precisa
dell'aritmetica della macchina; si può ipotizzare una precisione di almeno 256 bit. Esistono sei tipi di
costanti non impegnate, chiamate booleano non tipizzato, intero non tipizzato, runa non tipizzata,
virgola mobile non tipizzata, complesso non tipizzato e stringa non tipizzata.
Rinviando questo impegno, le costanti non tipizzate non solo mantengono la loro maggiore precisione
fino a un secondo momento, ma possono partecipare a molte più espressioni delle costanti
impegnate senza richiedere conversioni. Ad esempio, i valori ZiB e YiB dell'esempio precedente sono
troppo grandi per essere memorizzati in una variabile intera, ma sono costanti legittime che
possono essere utilizzate in espressioni come questa:
fmt.Println(YiB/ZiB) // "1024"

Come altro esempio, la costante in virgola mobile math.Pi può essere utilizzata ovunque sia necessario
un valore in virgola mobile o complesso:

www.it-ebooks.info
SEZIONE 3.6. COSTANTI 79

var x float32 = math.Pi var y


float64 = math.Pi var z
complex128 = math.Pi

Se math.Pi fosse stato impegnato per un tipo specifico, come float64, il risultato non sarebbe così preciso
e sarebbe necessario effettuare conversioni di tipo per utilizzarlo quando si desidera un valore
float32 o complex128:

const Pi64 float64 = math.Pi

var x float32 = float32(Pi64) var y


float64 = Pi64
var z complex128 = complex128(Pi64)

Per i letterali, la sintassi determina il sapore. I letterali 0, 0,0, 0i e '\u0000' denotano tutti con-
sistenti dello stesso valore ma di sapore diverso: intero non tipizzato, virgola mobile non tipizzato,
complesso non tipizzato e runa non tipizzata, rispettivamente. Allo stesso modo, true e false sono
booleani non tipizzati e i letterali di stringa sono stringhe non tipizzate.

Ricordiamo che / può rappresentare una divisione in numeri interi o in virgola mobile, a seconda dei
suoi operandi. Di conseguenza, la scelta del letterale può influenzare il risultato di un'espressione di
divisione costante:

var f float64 = 212


fmt.Println((f - 32) * 5 / 9) // "100"; (f - 32) * 5 è un float64
fmt.Println(5 / 9 * (f - 32)) // "0"; 5/9 è un intero non tipizzato, 0
fmt.Println(5.0 / 9.0 * (f - 32)) // "100"; 5,0/9,0 è un float non tipizzato

Solo le costanti possono essere non tipizzate. Quando una costante non tipizzata viene assegnata a una
variabile, come nella prima affermazione qui sotto, o compare sul lato destro di una dichiarazione di
variabile con un tipo esplicito, come nelle altre tre affermazioni, la costante viene implicitamente
convertita nel tipo della variabile, se possibile.

var f float64 = 3 + 0i // complesso non tipizzato -> float64 f =


2 // intero non tipizzato -> float64
f = 1e123 // virgola mobile non tipizzata -> float64
f = 'a' // runa non tipizzata -> float64

Le affermazioni precedenti sono quindi equivalenti a queste:

var f float64 = float64(3 + 0i) f =


float64(2)
f = float64(1e123) f
= float64('a')

Che sia implicita o esplicita, la conversione di una costante da un tipo a un altro richiede che il tipo di
destinazione possa rappresentare il valore originale. L'arrotondamento è consentito per i numeri reali e
complessi in virgola mobile:

www.it-ebooks.info
80 CAPITOLO 3. TIPI DI DATI DI BASE

const (
deadbeef = 0xdeadbeef // int non tipizzato con valore 3735928559 a =
uint32(deadbeef) // uint32 con valore 3735928559
b = float32(deadbeef) // float32 con valore 3735928576 (arrotondato per eccesso)
c = float64(deadbeef) // float64 con valore 3735928559 (esatto)
d = int32(deadbeef) // errore di compilazione: overflow costante int32
e = float64(1e309) // errore di compilazione: overflow costante
float64 f = uint(-1) // errore di compilazione: costante sottoflutto uint
)

In una dichiarazione di variabile senza un tipo esplicito (incluse le dichiarazioni di variabili brevi), il
sapore della costante non tipizzata determina implicitamente il tipo predefinito della variabile, come in
questi esempi:
i := 0 // intero non tipizzato; implicito int(0)
r := '\000' // runa non tipizzata; runa implicita('\000') f :=
0.0 // virgola mobile non tipizzata; implicito float64(0.0) c :=
0i // complesso non tipizzato; complesso implicito128(0i)

Si noti l'asimmetria: i numeri interi non tipizzati sono convertiti in int, la cui dimensione non è
garantita, ma i numeri a virgola mobile e complessi non tipizzati sono convertiti nei tipi esplicitamente
dimensionati float64 e complex128. Il linguaggio non ha tipi float e complex non dimensionati analoghi
a int non dimensionati, perché è molto difficile scrivere algoritmi numerici corretti senza conoscere le
dimensioni dei propri tipi di dati in virgola mobile.
Per dare alla variabile un tipo diverso, dobbiamo convertire esplicitamente la costante non tipizzata nel
tipo desiderato o dichiarare il tipo desiderato nella dichiarazione della variabile, come in questi esempi:
var i = int8(0)
var i int8 = 0

Questi valori predefiniti sono particolarmente importanti quando si converte una costante non tipizzata
in un valore di interfaccia (si veda il capitolo 7), poiché ne determinano il tipo dinamico.
fmt.Printf("%T\n", 0) // "int"
fmt.Printf("%T\n", 0.0) // "float64"
fmt.Printf("%T\n", 0i) // "complex128"
fmt.Printf("%T\n", '\000') // "int32" (runa)

Abbiamo ora trattato i tipi di dati di base di Go. Il passo successivo è mostrare come possono essere
combinati in raggruppamenti più grandi, come array e strutture, e poi in strutture di dati per risolvere
problemi di programmazione reali.

www.it-ebooks.info
4
Tipi di composito

Nel Capitolo 3 abbiamo parlato dei tipi di base che servono come elementi costitutivi delle strutture di
dati in un programma Go; sono gli atomi del nostro universo. In questo capitolo daremo un'occhiata ai
tipi compositi, le molecole create combinando i tipi di base in vari modi. Parleremo di quattro di questi
tipi: array, slices, map e struct e alla fine del capitolo mostreremo come i dati strutturati che utilizzano
questi tipi possono essere codificati e analizzati da dati JSON e utilizzati per generare HTML da modelli.

Gli array e le struct sono tipi aggregati; i loro valori sono concatenazioni di altri valori in memoria. Gli
array sono omogenei - i loro elementi hanno tutti lo stesso tipo - mentre le struct sono eterogenee. Sia gli
array che le struct hanno dimensioni fisse. Al contrario, le slices e le mappe sono strutture di dati
dinamiche che crescono con l'aggiunta di valori.

4.1. Array

Un array è una sequenza di lunghezza fissa di zero o più elementi di un particolare tipo. A causa della
loro lunghezza fissa, gli array sono raramente utilizzati direttamente in Go. Le slice, che possono
crescere e ridursi, sono molto più versatili, ma per capire le slice dobbiamo prima capire gli array.

I singoli elementi dell'array sono accessibili con la notazione convenzionale dei pedici, dove i pedici
vanno da zero a uno in meno della lunghezza dell'array. La funzione incorporata len restituisce il
numero di elementi dell'array.

var a [3]int // array di 3 interi fmt.Println(a[0])


// stampa il primo elemento
fmt.Println(a[len(a)-1]) // stampa l'ultimo elemento, a[2]

81

www.it-ebooks.info
82 CAPITOLO 4. TIPI DI COMPOSITI

// Stampa gli indici e gli elementi. for i, v


:= range a {
fmt.Printf("%d %d\n", i, v)
}

// Stampa solo gli elementi. for


_, v := range a {
fmt.Printf("%d\n", v)
}

Per impostazione predefinita, gli elementi di una nuova variabile di array sono inizialmente impostati
sul valore zero per il tipo di elemento, ovvero 0 per i numeri. È possibile utilizzare un letterale di array
per inizializzare un array con un elenco di valori:
var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // "0"

In un letterale di matrice, se al posto della lunghezza compare un'ellissi ''...'', la lunghezza della matrice è
determinata dal numero di inizializzatori. La definizione di q può essere semplificata in
q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"

La dimensione di un array fa parte del suo tipo, quindi [3]int e [4]int sono tipi diversi. La dimensione
deve essere un'espressione costante, cioè un'espressione il cui valore può essere calcolato durante la
compilazione del programma.
q := [3]int{1, 2, 3}
q = [4]int{1, 2, 3, 4} // errore di compilazione: impossibile assegnare [4]int a [3]int

Come vedremo, la sintassi letterale è simile per array, slices, mappe e strutture. La forma specifica di cui
sopra è un elenco di valori in ordine, ma è anche possibile specificare un elenco di coppie di indici e
valori, come questo:
tipo Valuta int

const (
Valuta USD = iota
EUR
GBP
RMB
)

simbolo := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"} fmt.Println(RMB,
simbolo[RMB]) // "3 ¥"

In questa forma, gli indici possono comparire in qualsiasi ordine e alcuni possono essere omessi; come prima,
i valori non specificati assumono il valore zero per il tipo di elemento. Ad esempio,
r := [...]int{99: -1}

definisce una matrice r con 100 elementi, tutti pari a zero tranne l'ultimo, che ha valore -1.

www.it-ebooks.info
SEZIONE 4.1. ARRAIE 83

Se il tipo di elemento di una matrice è comparabile, anche il tipo di matrice è comparabile, quindi
possiamo confrontare direttamente due matrici di quel tipo utilizzando l'operatore ==, che indica se tutti
gli elementi corrispondenti sono uguali. L'operatore != è la sua negazione.
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "vero falso falso" d := [3]int{1,
2}
fmt.Println(a == d) // errore di compilazione: impossibile confrontare [2]int == [3]int

Come esempio più plausibile, la funzione Sum256 del pacchetto crypto/sha256 produce l'hash crittografico
SHA256 o digest di un messaggio memorizzato in una fetta di byte arbitraria. Il digest ha 256 bit,
quindi il suo tipo è [32]byte. Se due digest sono uguali, è estremamente probabile che i due
messaggi siano uguali; se i digest differiscono, i due messaggi sono diversi. Questo programma stampa e
confronta i digest SHA256 di "x" e "X":
gopl.io/ch4/sha256
importare "crypto/sha256"

func main() {
c1 := sha256.Sum256([]byte("x"))
c2 := sha256.Sum256([]byte("X"))
fmt.Printf("%x\n%x\n%t\n%T\n", c1, c2, c1 == c2, c1)
// Uscita:
// 2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881
// 4b68ab3847feda7d6c62c1fbcbeebfa35eab7351ed5e78f4ddadea5df64b8015
// falso
// [32]uint8
}

I due input differiscono di un solo bit, ma circa la metà dei bit sono diversi nei digest. Notate i verbi
Printf: %x per stampare tutti gli elementi di un array o di una fetta di byte in esadecimale, %t per
mostrare un booleano e %T per visualizzare il tipo di un valore.
Quando si chiama una funzione, una copia di ogni valore dell'argomento viene assegnata alla variabile
parametro corrispondente, in modo che la funzione riceva una copia e non l'originale. Passare array di
grandi dimensioni in questo modo può essere inefficiente e qualsiasi modifica apportata dalla funzione
agli elementi dell'array influisce solo sulla copia, non sull'originale. A questo proposito, Go tratta gli
array come qualsiasi altro tipo, ma questo comportamento è diverso dai linguaggi che passano
implicitamente gli array per riferimento.
Naturalmente, è possibile passare esplicitamente un puntatore a un array, in modo che qualsiasi modifica
apportata dalla funzione agli elementi dell'array sia visibile al chiamante. Questa funzione azzera il
contenuto di un array
[32] array di byte :
func zero(ptr *[32]byte) { for i
:= range ptr {
ptr[i] = 0
}
}

www.it-ebooks.info
84 CAPITOLO 4. TIPI DI COMPOSITI

Il letterale di array [32]byte{} produce un array di 32 byte. Ogni elemento dell'array ha il valore zero per il
byte, cioè zero. Possiamo usare questo fatto per scrivere una versione diversa di zero:
func zero(ptr *[32]byte) {
*ptr = [32]byte{}
}

L'uso di un puntatore a un array è efficiente e consente alla funzione chiamata di modificare la


variabile del chiamante, ma gli array sono ancora intrinsecamente poco flessibili a causa della loro
dimensione fissa. La funzione zero non accetta un puntatore a una variabile di [16]byte, ad esempio,
né esiste un modo per aggiungere o rimuovere elementi di un array. Per questi motivi, a parte casi
speciali come l'hash a dimensione fissa di SHA256, gli array sono raramente usati come parametri
di funzione; al loro posto si usano le fette.
Esercizio 4.1: Scrivere una funzione che conti il numero di bit diversi in due hash SHA256. (Vedere
PopCount nella Sezione 2.6.2).
Esercizio 4.2: Scrivere un programma che stampi l'hash SHA256 del suo input standard per
impostazione predefinita, ma che supporti un flag da riga di comando per stampare invece l'hash
SHA384 o SHA512.

4.2. Fette

Le slice rappresentano sequenze di lunghezza variabile i cui elementi hanno tutti lo stesso tipo. Un tipo
di slice si scrive []T, dove gli elementi hanno tipo T; assomiglia a un tipo di array senza dimensione.
Gli array e le slice sono intimamente connessi. Una slice è una struttura dati leggera che dà accesso
a una sottosequenza (o forse a tutti) degli elementi di un array, noto come array sottostante alla slice.
Una slice ha tre componenti: un puntatore, una lunghezza e una capacità. Il puntatore punta al primo
elemento dell'array raggiungibile attraverso la slice, che non è necessariamente il primo elemento
dell'array. La lunghezza è il numero di elementi della slice; non può superare la capacità, che di solito è il
numero di elementi tra l'inizio della slice e la fine dell'array sottostante. Le funzioni integrate len e cap
restituiscono questi valori.

Più fette possono condividere lo stesso array sottostante e possono riferirsi a parti sovrapposte di tale
array. La Figura 4.1 mostra un array di stringhe per i mesi dell'anno e due fette sovrapposte di esso.
L'array è dichiarato come
mesi := [...]string{1: "gennaio", /* ... */, 12: "dicembre"}

quindi gennaio è mesi[1] e dicembre è mesi[12]. Normalmente, l'elemento dell'array all'indice 0


conterrebbe il primo valore, ma poiché i mesi sono sempre numerati a partire da 1, possiamo escluderlo
dalla dichiarazione e sarà inizializzato a una stringa vuota.
L'operatore slice s[i:j], dove 0 ≤ i ≤ j ≤ cap(s), crea una nuova slice che fa riferimento agli elementi
da i a j-1 della sequenza s, che può essere una variabile della matrice, un puntatore a una
matrice o un'altra slice. La slice risultante ha j-i elementi. Se i è omesso, è 0 e se j è omesso, è len(s).
Quindi la slice mesi[1:13] si riferisce all'intero intervallo di mesi validi, così come la slice mesi[1:]; la
slice mesi[:] si riferisce all'intero array. Definiamo le fette sovrapposte per il secondo trimestre e l'estate
boreale:

www.it-ebooks.info
SEZIONE 4.2. 85
SCALATURE

Figura 4.1. Due fette sovrapposte di una matrice di mesi.

Q2 := mesi[4:7] estate :=
mesi[6:9]
fmt.Println(Q2) // ["Aprile" "Maggio" "Giugno"]
fmt.Println(estate) // ["Giugno" "Luglio" "Agosto"]

Giugno è incluso in ciascuno di essi ed è l'unico risultato di questo test (inefficiente) per gli elementi comuni:

per _, s := range estate { per _,


q := range Q2 {
se s == q {
fmt.Printf("%s appare in entrambi", s)
}
}
}

www.it-ebooks.info
86 CAPITOLO 4. TIPI DI COMPOSITI

L'affettatura oltre il/i tappo/i provoca un panico, ma l'affettatura oltre len(i) estende la fetta, quindi
il risultato può essere più lungo dell'originale:
fmt.Println(estate[:20]) // panico: fuori campo
endlessSummer := estate[:5] // estende una fetta (entro la capacità) fmt.Println(endlessSummer)
// "[giugno luglio agosto settembre ottobre]"

A titolo di esempio, si noti la somiglianza tra l'operazione substring sulle stringhe e l'operatore slice sulle
fette di []byte. Entrambe si scrivono x[m:n] ed entrambe restituiscono una sottosequenza dei byte
originali, condividendo la rappresentazione sottostante in modo che entrambe le operazioni richiedano
un tempo costante. L'espressione x[m:n] restituisce una stringa se x è una stringa, o un []byte se x è
un []byte.
Poiché una slice contiene un puntatore a un elemento di un array, il passaggio di una slice a una
funzione consente a quest'ultima di modificare gli elementi dell'array sottostante. In altre parole, copiare
una slice crea un alias (§2.3.2) per l'array sottostante. La funzione reverse inverte gli elementi di una
slice []int e può essere applicata a slice di qualsiasi lunghezza.
gopl.io/ch4/rev
// reverse inverte una fetta di ints in posizione.
func reverse(s []int) {
per i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}

Qui invertiamo l'intero array a:


a := [...]int{0, 1, 2, 3, 4, 5}
reverse(a[:])
fmt.Println(a) // "[5 4 3 2 1 0]"

Un modo semplice per ruotare una fetta di n elementi verso sinistra consiste nell'applicare la
funzione inversa tre volte, prima agli n elementi principali, poi agli elementi rimanenti e infine
all'intera fetta. (Per ruotare a destra, bisogna fare prima la terza chiamata).
s := []int{0, 1, 2, 3, 4, 5}
// Ruota s a sinistra di due posizioni. reverse(s[:2])
reverse(s[2:])
reverse(s)
fmt.Println(s) // "[2 3 4 5 0 1]"

Si noti come l'espressione che inizializza la slice s sia diversa da quella per l'array a. Un letterale di slice
assomiglia a un letterale di array, una sequenza di valori separati da virgole e circondati da parentesi
graffe, ma la dimensione non è data. In questo modo si crea implicitamente una variabile di array della
giusta dimensione e si ottiene una slice che punta ad essa. Come per i letterali di array, i letterali di slice
possono specificare i valori in ordine, oppure fornire i loro indici in modo esplicito, o usare un mix dei
due stili.
A differenza degli array, le slice non sono confrontabili, quindi non si può usare == per verificare se due
slice contengono gli stessi elementi. La libreria standard fornisce la funzione bytes.Equal, altamente
ottimizzata, per confrontare due fette di byte ([]byte), ma per altri tipi di fette è necessario eseguire la
funzione

www.it-ebooks.info
SEZIONE 4.2. 87
SCALATURE

confronto con noi stessi:


func equal(x, y []string) bool { if
len(x) != len(y) {
restituire false
}
for i := range x { if
x[i] != y[i] {
restituire false
}
}
restituire vero
}

Data la naturalezza di questo test di uguaglianza "profondo" e il fatto che non è più costoso in fase di
esecuzione del metodo
== per gli array di stringhe, può lasciare perplessi il fatto che anche i confronti tra slice non funzionino in
questo modo. L'equivalenza profonda è problematica per due motivi. In primo luogo, a differenza degli
elementi di un array, gli elementi di una slice sono indiretti, il che rende possibile che una slice contenga
se stessa. Sebbene esistano modi per gestire questi casi, nessuno è semplice, efficiente e, soprattutto,
ovvio.
In secondo luogo, poiché gli elementi della slice sono indiretti, un valore fisso della slice può contenere
elementi diversi in momenti diversi, man mano che il contenuto dell'array sottostante viene modificato.
Poiché una tabella hash come il tipo map di Go crea solo copie superficiali delle sue chiavi, richiede che
l'uguaglianza per ogni chiave rimanga la stessa per tutta la durata della tabella hash. Un'equivalenza
profonda renderebbe quindi le fette inadatte a essere utilizzate come chiavi di una mappa. Per i tipi di
riferimento come i puntatori e i canali, l'opzione
L'operatore == verifica l'identità dei riferimenti, cioè se le due entità si riferiscono alla stessa cosa. Un
analogo test di uguaglianza ''superficiale'' per le fette potrebbe essere utile e risolverebbe il problema delle
mappe, ma il trattamento incoerente di fette e array da parte dell'operatore == creerebbe confusione. La
scelta più sicura è quella di non ammettere del tutto i confronti tra le slice.
L'unico confronto legale tra slice è contro nil, come in
if summer == nil { /* ... */ }

Il valore zero di un tipo di slice è nil. Una slice nil non ha un array sottostante. La slice nil ha
lunghezza e capacità zero, ma esistono anche slice non nil di lunghezza e capacità zero, come []int{}
o make([]int, 3)[3:]. Come per ogni tipo che può avere valori nil, il valore nil di un particolare tipo
di slice può essere scritto usando un'espressione di conversione come []int(nil).
var s []int // len(s) == 0, s == nil s = nil
// len(s) == 0, s == nil s =
[]int(nil) // len(s) == 0, s == nil s = []int{}
// len(s) == 0, s = nil

Quindi, se si vuole verificare se una slice è vuota, si usa len(s) == 0, non s == nil. A parte il
confronto con nil, una slice nil si comporta come qualsiasi altra slice di lunghezza zero; ad esempio,
reverse(nil) è perfettamente sicuro. A meno che non sia chiaramente documentato il contrario, le
funzioni di Go dovrebbero trattare tutti gli slice di lunghezza zero allo stesso modo, che siano nil o
non nil.

www.it-ebooks.info
88 CAPITOLO 4. TIPI DI COMPOSITI

La funzione incorporata make crea una fetta di un elemento di tipo, lunghezza e capacità specificati.
L'argomento capacità può essere omesso, nel qual caso la capacità equivale alla lunghezza.
make([]T, len)
make([]T, len, cap) // come make([]T, cap)[:len]

Sotto il cofano, make crea una variabile array senza nome e ne restituisce una fetta; l'array è accessibile
solo attraverso la fetta restituita. Nella prima forma, la fetta è una vista dell'intero array. Nella
seconda, la slice è una vista solo dei primi len elementi dell'array, ma la sua capacità include l'intero
array. Gli elementi aggiuntivi vengono messi da parte per una crescita futura.

4.2.1. La funzione append

La funzione incorporata append aggiunge elementi alle fette:


var rune []rune
per _, r := intervallo "Ciao, B$" { rune =
append(rune, r)
}
fmt.Printf("%q\n", rune) // "['H' 'e' 'l' 'l' 'o' ',' ' ' 'B' '$']"

Il ciclo utilizza append per costruire la fetta di nove rune codificate dalla stringa letterale, anche se questo
problema specifico è più convenientemente risolto utilizzando la conversione integrata []rune("Hello,
B$").

La funzione append è fondamentale per capire come funzionano le fette, quindi diamo un'occhiata a cosa
succede. Ecco una versione chiamata appendInt, specializzata per le fette []int:
gopl.io/ch4/append
func appendInt(x []int, y int) []int { var z
[]int
zlen := len(x) + 1 if
zlen <= cap(x) {
// C'è spazio per crescere. Estendere la fetta. z =
x[:zlen]
} else {
// Lo spazio è insufficiente. Allocare un nuovo array.
// Crescita per raddoppio, per una complessità lineare
ammortizzata. zcap := zlen
if zcap < 2*len(x) { zcap =
2 * len(x)
}
z = make([]int, zlen, zcap)
copy(z, x) // una funzione integrata; vedi testo
}
z[len(x)] = y
return z
}

www.it-ebooks.info
SEZIONE 4.2. 89
SCALATURE

Ogni chiamata ad appendInt deve verificare se la slice ha una capacità sufficiente per contenere i nuovi
elementi nell'array esistente. In caso affermativo, estende la slice definendo una slice più grande (sempre
all'interno dell'array originale), copia l'elemento y nel nuovo spazio e restituisce la slice. L'input x e il
risultato z condividono lo stesso array sottostante.

Se lo spazio a disposizione per la crescita è insufficiente, appendInt deve allocare un nuovo array
sufficientemente grande per contenere il risultato, copiarvi i valori di x e quindi aggiungere il nuovo
elemento y. Il risultato z ora si riferisce a un array sottostante diverso da quello a cui si riferisce x.

Sarebbe semplice copiare gli elementi con cicli espliciti, ma è più facile usare la funzione built-in copy,
che copia gli elementi da una slice a un'altra dello stesso tipo. Il suo primo argomento è la
destinazione e il secondo è l'origine, in modo simile all'ordine degli operandi in un'assegnazione come
dst = src. Le slice possono riferirsi allo stesso array sottostante; possono anche sovrapporsi. Anche se
non lo usiamo in questo caso, copy restituisce il numero di elementi effettivamente copiati, che è il
più piccolo delle due lunghezze delle fette, quindi non c'è il pericolo di andare fuori limite o di
sovrascrivere qualcosa fuori dal raggio d'azione.

Per motivi di efficienza, il nuovo array è di solito un po' più grande del minimo necessario per contenere
x e y. L'espansione dell'array raddoppiando la sua dimensione a ogni espansione evita un numero
eccessivo di allocazioni e garantisce che l'aggiunta di un singolo elemento richieda in media un tempo
costante. Questo programma ne dimostra l'effetto:
func main() {
var x, y []int
for i := 0; i < 10; i++ { y =
appendInt(x, i)
fmt.Printf("%d cap=%d\t%v\n", i, cap(y), y) x = y
}
}

Ogni variazione di capacità indica un'assegnazione e una copia:


0 cap=1 [0]
1 cap=2 [0 1]
2 cap=4 [0 1 2]
3 cap=4 [0 1 2 3]
4 cap=8 [0 1 2 3 4]
5 cap=8 [0 1 2 3 4 5]
6 cap=8 [0 1 2 3 4 5 6]
7 cap=8 [0 1 2 3 4 5 6 7]
8 cap=16 [ 0 1 2 3 4 5 6 7 8]
9 cap=16 [ 0 1 2 3 4 5 6 7 8 9]

Diamo un'occhiata più da vicino all'iterazione i=3. La slice x contiene i tre elementi [0 1 2] ma ha
capacità 4, quindi c'è un singolo elemento di slack alla fine e appendInt dell'elemento 3 può procedere
senza riallocare. La slice y risultante ha lunghezza e capacità 4 e ha lo stesso array sottostante della slice
x originale, come mostra la Figura 4.2.

www.it-ebooks.info
90 CAPITOLO 4. TIPI DI COMPOSITI

Figura 4.2. Aggiunta con spazio di crescita.

All'iterazione successiva, i=4, non c'è alcuno slack, quindi appendInt alloca un nuovo array di dimensione
8, copia i quattro elementi [0 1 2 3] di x e aggiunge 4, il valore di i. La slice y risultante ha una
lunghezza di 5 ma una capacità di 8; lo slack di 3 eviterà alle tre iterazioni successive la necessità di
riallocare. Le slice y e x sono viste di array diversi. Questa operazione è illustrata nella Figura 4.3.

Figura 4.3. Aggiunta senza spazio di crescita.

La funzione built-in append può utilizzare una strategia di crescita più sofisticata di quella semplicistica
di appendInt. Di solito non sappiamo se una determinata chiamata ad append causerà una riallocazione,
quindi non possiamo assumere che la slice originale si riferisca allo stesso array della slice risultante, né
che si riferisca a uno diverso. Allo stesso modo, non si può assumere che le operazioni sugli elementi
della vecchia slice si riflettano (o meno) nella nuova slice. Di conseguenza, è normale assegnare il
risultato di una chiamata ad append alla stessa variabile della slice il cui valore è stato passato ad append:
rune = append(rune, r)

www.it-ebooks.info
SEZIONE 4.2. 91
SCALATURE

L'aggiornamento della variabile slice è necessario non solo quando si chiama append, ma per qualsiasi
funzione che possa modificare la lunghezza o la capacità di una slice o farla riferire a un array sottostante
diverso. Per utilizzare correttamente le slice, è importante tenere presente che, sebbene gli elementi
dell'array sottostante siano indiretti, il puntatore, la lunghezza e la capacità della slice non lo sono. Per
aggiornarli è necessaria un'assegnazione come quella sopra descritta. In questo senso, le slice non sono
tipi di riferimento "puri", ma assomigliano a un tipo aggregato come questa struct:
type IntSlice struct { ptr
*int
len, cap int
}

La nostra funzione appendInt aggiunge un singolo elemento a una slice, ma il built-in append ci consente
di aggiungere più di un nuovo elemento, o addirittura un'intera slice.
var x []int
x = append(x, 1)
x = append(x, 2, 3)
x = append(x, 4, 5, 6)
x = append(x, x...) // aggiunge la slice x fmt.Println(x)
// "[1 2 3 4 5 6 1 2 3 4 5 6]"

Con la piccola modifica mostrata di seguito, si può ottenere il comportamento della funzione append
integrata. L'ellissi ''...'' nella dichiarazione di appendInt rende la funzione variadica: accetta qualsiasi
numero di argomenti finali. L'ellissi corrispondente nella chiamata ad append mostra come fornire un
elenco di argomenti da una slice. Spiegheremo questo meccanismo in dettaglio nella Sezione 5.7.
func appendInt(x []int, y ...int) []int { var z
[]int
zlen := len(x) + len(y)
// ...espandere z almeno fino a zlen...
copy(z[len(x):], y)
restituire z
}

La logica di espansione dell'array sottostante di z rimane invariata e non viene mostrata.

4.2.2. Tecniche di affettatura in loco

Vediamo altri esempi di funzioni che, come rotazione e inversione, modificano gli elementi di una slice sul
posto. Dato un elenco di stringhe, la funzione nonempty restituisce quelle non vuote:
gopl.io/ch4/non vuoto
// Nonempty è un esempio di algoritmo di slice in-place. package main

importare "fmt"

www.it-ebooks.info
92 CAPITOLO 4. TIPI DI COMPOSITI

// nonempty restituisce una slice contenente solo le stringhe non vuote.


// L'array sottostante viene modificato durante la chiamata.
func nonempty(stringhe []string) []string {
i := 0
for _, s := range strings { if s
!= "" {
stringhe[i] = s i++
}
}
restituire stringhe[:i]
}

La parte sottile è che la slice di input e quella di output condividono lo stesso array sottostante. Questo
evita la necessità di allocare un altro array, anche se ovviamente il contenuto dei dati viene in parte
sovrascritto, come dimostra la seconda istruzione di stampa:
data := []string{"uno", "", "tre"} fmt.Printf("%q\n",
nonempty(data)) // `["uno" "tre"]`
fmt.Printf("%q\n", dati) // `["uno" "tre" "tre"]`

Di solito si scrive: data = nonempty(data). La funzione nonempty


può essere scritta anche utilizzando append:
func nonempty2(stringhe []string) []string {
out := string[:0] // fetta a lunghezza zero dell'originale
for _, s := range stringhe {
se s != "" {
out = append(out, s)
}
}
ritorno fuori
}

Qualunque sia la variante utilizzata, il riutilizzo di un array in questo modo richiede che venga prodotto
al massimo un valore di uscita per ogni valore di ingresso, il che è vero per molti algoritmi che filtrano gli
elementi di una sequenza o ne combinano di adiacenti. L'uso di queste fette intricate è l'eccezione, non la
regola, ma può essere chiaro, efficiente e utile in alcune occasioni.
Una slice può essere utilizzata per implementare una pila. Data una pila di slice inizialmente vuota,
possiamo inserire un nuovo valore alla fine della slice con append:
stack = append(stack, v) // push v

La cima della pila è l'ultimo elemento:


top := stack[len(stack)-1] // cima della pila

e la riduzione della pila con l'eliminazione di quell'elemento è


stack = stack[:len(stack)-1] // pop

www.it-ebooks.info
SEZIONE 4.3. MAPPE 93

Per rimuovere un elemento dal centro di una fetta, preservando l'ordine degli elementi rimanenti,
utilizzare la funzione di copia per far scorrere gli elementi di numero superiore verso il basso di uno per
riempire lo spazio:
func remove(slice []int, i int) []int {
copy(slice[i:], slice[i+1:])
restituire slice[:len(slice)-1]
}
func main() {
s := []int{5, 6, 7, 8, 9}
fmt.Println(remove(s, 2)) // "[5 6 8 9]"
}

E se non abbiamo bisogno di preservare l'ordine, possiamo semplicemente spostare l'ultimo elemento nello
spazio:
func remove(slice []int, i int) []int {
slice[i] = slice[len(slice)-1] return
slice[:len(slice)-1]
}
func main() {
s := []int{5, 6, 7, 8, 9}
fmt.Println(remove(s, 2)) // "[5 6 9 8]
}

Esercizio 4.3: Riscrivere l'inverso per utilizzare un puntatore ad un array invece di una slice.
Esercizio 4.4: Scrivere una versione di rotate che operi in un solo passaggio.
Esercizio 4.5: Scrivere una funzione in-place per eliminare i duplicati adiacenti in una slice []stringa.
Esercizio 4.6: Scrivere una funzione in-place che schiacci ogni serie di spazi Unicode adiacenti (vedere
unicode.IsSpace) in una slice []byte codificata UTF-8 in un singolo spazio ASCII.
Esercizio 4.7: Modificare reverse per invertire i caratteri di una slice []byte che rappresenta una
stringa codificata in UTF-8, in posizione. È possibile farlo senza allocare nuova memoria?

4.3. Mappe

La tabella hash è una delle strutture dati più ingegnose e versatili. Si tratta di un insieme non ordinato di
coppie chiave/valore in cui tutte le chiavi sono distinte e il valore associato a una determinata chiave può
essere recuperato, aggiornato o rimosso utilizzando un numero costante di confronti tra chiavi, in
media, indipendentemente dalle dimensioni della tabella hash.
In Go, una mappa è un riferimento a una tabella hash e un tipo di mappa si scrive map[K]V, dove K e V
sono i tipi delle chiavi e dei valori. Tutte le chiavi di una data mappa sono dello stesso tipo e tutti i valori
sono dello stesso tipo, ma non è necessario che le chiavi siano dello stesso tipo dei valori. Il tipo di chiave
K deve essere confrontabile tramite ==, in modo che la mappa possa verificare se una determinata chiave
è uguale a una già presente al suo interno. Sebbene i numeri in virgola mobile siano confrontabili, è una
cattiva idea confrontare i numeri in virgola mobile per l'uguaglianza e, come abbiamo detto nel Capitolo
3, è particolarmente sbagliato se NaN è un valore possibile. Non ci sono restrizioni sul tipo di valore V.

www.it-ebooks.info
94 CAPITOLO 4. TIPI DI COMPOSITI

La funzione incorporata make può essere utilizzata per creare una mappa:
ages := make(map[string]int) // mappatura da stringhe a ints

Possiamo anche usare un letterale di mappa per creare una nuova mappa popolata con alcune coppie
chiave/valore iniziali:
età := map[string]int{
"alice": 31,
"charlie": 34,
}

Ciò equivale a
ages := make(map[string]int)
ages["alice"] = 31
età["charlie"] = 34

quindi un'espressione alternativa per una nuova mappa vuota è


map[string]int{}. L'accesso agli elementi della mappa avviene tramite la

consueta notazione dei pedici:


età["alice"] = 32
fmt.Println(ages["alice"]) // " 32"

e rimossi con la funzione integrata delete:


delete(ages, "alice") // rimuove l'elemento ages["alice"].

Tutte queste operazioni sono sicure anche se l'elemento non è presente nella mappa; una ricerca su
una mappa che utilizza una chiave non presente restituisce il valore zero per il suo tipo, quindi, per
esempio, la seguente operazione funziona anche se "bob" non è ancora una chiave della mappa,
perché il valore di ages["bob"] sarà 0.
ages["bob"] = ages["bob"] + 1 // buon compleanno!

Le forme di assegnazione abbreviata x += y e x++ funzionano anche per gli elementi della mappa, quindi
possiamo riscrivere l'istruzione precedente come
ages["bob"] += 1

o ancora più concisamente come


età["bob"]++

Ma un elemento della mappa non è una variabile e non si può prendere il suo indirizzo:
_ = &ages["bob"] // errore di compilazione: impossibile prendere l'indirizzo di un elemento della
mappa

Un motivo per cui non si può prendere l'indirizzo di un elemento della mappa è che la crescita di una
mappa potrebbe causare il rehashing degli elementi esistenti in nuove posizioni di memoria, invalidando
così potenzialmente l'indirizzo.
Per enumerare tutte le coppie chiave/valore della mappa, utilizziamo un ciclo for basato su un intervallo,
simile a q u e l l o visto per le fette. Le iterazioni successive del ciclo fanno sì che le variabili name ed
age vengano impostate sulla coppia chiave/valore successiva:

www.it-ebooks.info
SEZIONE 4.3. MAPPE 95

per nome, età := intervallo di età {


fmt.Printf("%s\t%d\n", nome, età)
}

L'ordine di iterazione delle mappe non è specificato e diverse implementazioni potrebbero utilizzare una
funzione di hash diversa, che porta a un ordine diverso. In pratica, l'ordine è casuale e varia da
un'esecuzione all'altra. Questo è intenzionale: far variare la sequenza aiuta a forzare i programmi a essere
robusti tra le varie implementazioni. Per enumerare le coppie chiave/valore in ordine, dobbiamo
ordinare le chiavi in modo esplicito, per esempio usando la funzione Strings del pacchetto sort se le
chiavi sono stringhe. Questo è uno schema comune:
importare "ordinamento"
var nomi []stringa
per nome := intervallo di età {
nomi = append(nomi, nome)
}
sort.Strings(nomi)
for _, name := range names { fmt.Printf("%s\t%d\n",
name, ages[name])
}

Poiché conosciamo la dimensione finale dei nomi fin dall'inizio, è più efficiente allocare un array della
dimensione richiesta in anticipo. L'istruzione seguente crea una slice inizialmente vuota, ma con una
capacità sufficiente a contenere tutte le chiavi della mappa ages:
nomi := make([]string, 0, len(ages))

Nel primo ciclo di range qui sopra, abbiamo bisogno solo delle chiavi della mappa ages, quindi
omettiamo la seconda variabile del ciclo. Nel secondo ciclo, abbiamo bisogno solo degli elementi della
slice dei nomi, quindi usiamo l'identificatore vuoto _ per ignorare la prima variabile, l'indice.
Il valore zero per un tipo di mappa è nil, cioè un riferimento a nessuna tabella hash.
var ages map[string]int fmt.Println(ages ==
nil) // "vero"
fmt.Println(len(ages) == 0) // "vero"

La maggior parte delle operazioni sulle mappe, compresi i cicli di lookup, delete, len e range, sono
sicure da eseguire su un riferimento di mappa nullo, poiché si comporta come una mappa vuota.
Ma la memorizzazione su una mappa nil provoca un panico:
ages["carol"] = 21 // panico: assegnazione a una voce nella mappa nil

È necessario allocare la mappa prima di poterla memorizzare.


L'accesso a un elemento della mappa tramite pedice produce sempre un valore. Se la chiave è
presente nella mappa, si ottiene il valore corrispondente; in caso contrario, si ottiene il valore zero per il
tipo di elemento, come abbiamo visto con ages["bob"]. Per molti scopi questo va bene, ma a volte è
necessario sapere se l'elemento era davvero presente o meno. Per esempio, se il tipo di elemento è
numerico, si potrebbe dover distinguere tra un elemento inesistente e un elemento che ha per caso il
valore zero, usando un test come questo:

www.it-ebooks.info
96 CAPITOLO 4. TIPI DI COMPOSITI

età, ok := età["bob"]
if !ok { /* "bob" non è una chiave in questa mappa; age == 0. */ }

Spesso si vedono queste due affermazioni combinate, come in questo caso:


if age, ok := ages["bob"]; !ok { /* ... */ }

La pedice di una mappa in questo contesto produce due valori; il secondo è un booleano che indica
se l'elemento è presente. La variabile booleana viene spesso chiamata ok, soprattutto se viene
utilizzata immediatamente in una condizione if.

Come per le fette, le mappe non possono essere confrontate tra loro; l'unico confronto legale è quello
con nil. Per verificare se due mappe contengono le stesse chiavi e gli stessi valori associati, dobbiamo
scrivere un ciclo:
func equal(x, y map[string]int) bool { if
len(x) != len(y) {
restituire false
}
per k, xv := intervallo x {
if yv, ok := y[k]; !ok || yv != xv { return
false
}
}
restituire vero
}

Osservate come usiamo !ok per distinguere i casi "mancante" e "presente ma zero". Se avessimo
scritto ingenuamente xv != y[k], la chiamata sottostante riporterebbe erroneamente i suoi argomenti
come uguali:
// Vero se equal è scritto in modo errato.
equal(map[string]int{"A": 0}, map[string]int{"B": 42})

Go non fornisce un tipo di insieme, ma poiché le chiavi di una mappa sono distinte, una mappa può
servire a questo scopo. Per illustrare, il programma dedup legge una sequenza di righe e stampa solo
la prima occorrenza di ogni riga distinta. (Il programma dedup utilizza una mappa le cui chiavi
rappresentano l'insieme delle righe già apparse per garantire che le occorrenze successive non
vengano stampate.
gopl.io/ch4/dedup
func main() {
seen := make(map[string]bool) // un insieme di stringhe
input := bufio.NewScanner(os.Stdin)
per input.Scan() {
line := input.Text() if
!seen[line] {
seen[line] = true
fmt.Println(line)
}
}

www.it-ebooks.info
SEZIONE 4.3. MAPPE 97

if err := input.Err(); err != nil { fmt.Fprintf(os.Stderr,


"dedup: %v\n", err) os.Exit(1)
}
}

I programmatori di Go spesso descrivono una mappa usata in questo modo come un ''insieme di
stringhe'' senza ulteriori spiegazioni, ma attenzione, non tutti i valori di map[string]bool sono semplici
insiemi; alcuni possono contenere sia valori veri che falsi.

A volte abbiamo bisogno di una mappa o di un insieme le cui chiavi sono fette, ma poiché le chiavi di
una mappa devono essere com- parabili, questo non può essere espresso direttamente. Tuttavia, è
possibile farlo in due fasi. Per prima cosa definiamo una funzione helper k che mappa ogni chiave in una
stringa, con la proprietà che k(x) == k(y) se e solo se consideriamo x e y equivalenti. Quindi creiamo una
mappa le cui chiavi sono stringhe, applicando la funzione helper a ogni chiave prima di accedere alla
mappa.

L'esempio seguente utilizza una mappa per registrare il numero di volte in cui Add è stato chiamato con
un dato elenco di stringhe. Utilizza fmt.Sprintf per convertire una fetta di stringhe in una singola stringa
che sia una chiave di mappa adatta, citando ogni elemento della fetta con %q per registrare fedelmente i
confini delle stringhe:
var m = make(map[string]int)

func k(list []string) string { return fmt.Sprintf("%q", list) } func

Add(list []string) { m[k(lista)]++ }


func Count(list []string) int { return m[k(list)] }

Lo stesso approccio può essere utilizzato per qualsiasi tipo di chiave non comparabile, non solo per le
fette. È utile anche per i tipi di chiave comparabili quando si vuole una definizione di uguaglianza
diversa da ==, come nel caso di confronti senza distinzione tra maiuscole e minuscole per le stringhe.
Inoltre, non è necessario che il tipo di k(x) sia una stringa; va bene qualsiasi tipo comparabile con la
proprietà di equivalenza desiderata, come interi, array o strutture.

Ecco un altro esempio di mappe in azione, un programma che conta le occorrenze di ogni punto di
codice Unicode distinto nel suo input. Poiché esiste un gran numero di caratteri possibili, di cui solo una
piccola parte appare in un particolare documento, una mappa è un modo naturale per tenere traccia solo
di quelli che sono stati visti e dei relativi conteggi.
gopl.io/ch4/charcount
// Charcount calcola il conteggio dei caratteri Unicode. pacchetto
main

importare (
"bufio"
"fmt"
"io"
"os"
"unicode"
"unicode/utf8"
)

www.it-ebooks.info
98 CAPITOLO 4. TIPI DI COMPOSITI

func main() {
counts := make(map[rune]int) // conta dei caratteri Unicode
var utflen [utf8.UTFMax + 1]int // conteggio delle lunghezze delle codifiche UTF-8
invalid := 0 // conteggio dei caratteri UTF-8 non validi

in := bufio.NewReader(os.Stdin) per {
r, n, err := in.ReadRune() // restituisce runa, nbyte, errore if err ==
io.EOF {
pausa
}
if err != nil {
fmt.Fprintf(os.Stderr, "charcount: %v\n", err)
os.Exit(1)
}
se r == unicode.ReplacementChar && n == 1 { invalid++
continuare
}
conta[r]++
utflen[n]++
}
fmt.Printf("rune\tcount\n") per
c, n := range counts {
fmt.Printf("%q\t%d\n", c, n)
}
fmt.Print("\nlen\tcount\n") per
i, n := range utflen {
se i > 0 {
fmt.Printf("%d\t%d\n", i, n)
}
}
se non valido > 0 {
fmt.Printf("\n%d caratteri UTF-8 non validi", invalido)
}
}

Il metodo ReadRune esegue la decodifica UTF-8 e restituisce tre valori: la runa decodificata, la lunghezza
in byte della sua codifica UTF-8 e un valore di errore. L'unico errore previsto è la fine del file. Se l'input
non era una codifica UTF-8 legale di una runa, la runa restituita è uni- code.ReplacementChar e la
sua lunghezza è 1.
Il programma charcount stampa anche un conteggio delle lunghezze delle codifiche UTF-8 delle rune
che compaiono nell'input. Una mappa non è la struttura dati migliore per questo scopo; poiché le
lunghezze delle codifiche vanno solo da 1 a utf8.UTFMax (che ha il valore 4), un array è più compatto.
Come esperimento, a un certo punto abbiamo eseguito il conteggio dei caratteri su questo stesso libro.
Sebbene sia per lo più in inglese, ovviamente, presenta un discreto numero di caratteri non-ASCII. Ecco
i primi dieci:
° 27 B 15 $ 14 é 13 * 10 c 5 × 5 D 4 d 4 ◻ 3

www.it-ebooks.info
SEZIONE 4.4. STRUTTURE 99

ed ecco la distribuzione delle lunghezze di tutte le codifiche UTF-8:


len conteggio
1 765391
2 60
3 70
4 0

Il tipo di valore di una mappa può essere esso stesso un tipo composito, come una mappa o una slice. Nel
codice seguente, il tipo chiave di graph è string e il tipo valore è map[string]bool, che rappresenta
un insieme di stringhe. Concettualmente, graph mappa una stringa a un insieme di stringhe correlate, i
suoi successori in un grafo diretto.
gopl.io/ch4/graph
var graph = make(map[string]map[string]bool)
func addEdge(from, to string) { edges :=
graph[from]
if edges == nil {
edges = make(map[string]bool) graph[from] =
edges
}
bordi[to] = vero
}
func hasEdge(from, to string) bool { return
graph[from][to]
}

La funzione addEdge mostra il modo idiomatico di popolare una mappa in modo pigro, cioè di
inizializzare ogni valore quando la sua chiave appare per la prima volta. La funzione hasEdge
mostra come il valore zero di una voce mancante della mappa sia spesso utilizzato: anche se
non sono presenti né from né to, graph[from][to] darà sempre un risultato significativo.
Esercizio 4.8: Modificare charcount per contare lettere, cifre e così via nelle loro categorie Unicode,
utilizzando funzioni come unicode.IsLetter.
Esercizio 4.9: Scrivete un programma wordfreq per riportare la frequenza di ogni parola in un file di
testo in ingresso. Chiamate input.Split(bufio.ScanWords) prima della prima chiamata a Scan per
suddividere l'input in parole anziché in righe.

4.4. Strutture

Una struct è un tipo di dati aggregato che raggruppa zero o più valori nominati di tipo arbitrario come
un'unica entità. Ogni valore è chiamato campo. L'esempio classico di struct nell'elaborazione dei dati è il
record del dipendente, i cui campi sono un ID univoco, il nome del dipendente, l'indirizzo, la data di
nascita, la posizione, lo stipendio, il manager e simili. Tutti questi campi sono raccolti in un'entità unica
che può essere copiata come unità, passata alle funzioni e restituita da queste, memorizzata in array e
così via.

www.it-ebooks.info
100 CAPITOLO 4. TIPI DI COMPOSITI

Queste due dichiarazioni dichiarano un tipo di struct chiamato Employee e una variabile chiamata
dilbert che è un'istanza di un Employee:

tipo Dipendente struct {


ID int
Nome stringa
Indirizzo stringa DoB
time.Time
Posizione stringa
Stipendio int
ManagerID int
}

var dilbert Dipendente

I singoli campi di dilbert sono accessibili usando la notazione a punti, come dilbert.Name e
dilbert.DoB. Poiché dilbert è una variabile, anche i suoi campi sono variabili, quindi si può
assegnare a un campo:
dilbert.Salary -= 5000 // retrocesso, per aver scritto troppo poche righe di codice

o prendere il suo indirizzo e accedervi tramite un puntatore:


posizione := &dilbert.Position
*posizione = "Senior " + *posizione // promosso, per esternalizzazione a Elbonia

La notazione a punti funziona anche con un puntatore a una struct:


var employeeOfTheMonth *Employee = &dilbert employeeOfTheMonth.Position += "
(proactive team player)"

L'ultima affermazione è equivalente a


(*impiegatoDelMese).Position += " (giocatore di squadra proattivo)"

Dato l'ID univoco di un dipendente, la funzione EmployeeByID restituisce un puntatore a un dipendente


struct. Possiamo utilizzare la notazione a punti per accedere ai suoi campi:
func EmployeeByID(id int) *Dipendente { /* ... */ }

fmt.Println(EmployeeByID(dilbert.ManagerID).Position) // "Capo con i capelli a punta" id :=

dilbert.ID
EmployeeByID(id).Salary = 0 // licenziato per... nessun motivo reale

L'ultima istruzione aggiorna la struttura Employee a cui punta il risultato della chiamata a
EmployeeByID. Se il tipo di risultato di EmployeeByID fosse cambiato in Employee invece di
*Dipendente, l'istruzione di assegnazione non verrebbe compilata poiché il suo lato sinistro non
identificherebbe una variabile.

I campi sono solitamente scritti uno per riga, con il nome del campo che precede il suo tipo, ma è
possibile combinare campi consecutivi dello stesso tipo, come nel caso di Nome e Indirizzo:

www.it-ebooks.info
SEZIONE 4.4. STRUTTURE 101

tipo Dipendente struct {


ID int
Nome, Indirizzo stringa DoB
time.Time
Posizione stringa
Stipendio int
ManagerID int
}

L'ordine dei campi è importante per l'identità del tipo. Se avessimo combinato anche la dichiarazione del
campo Posi- zione (anch'esso una stringa), o avessimo scambiato Nome e Indirizzo, avremmo definito un
tipo di struct diverso. Di solito si combinano solo le dichiarazioni di campi correlati.
Il nome di un campo di una struct è esportato se inizia con una lettera maiuscola; questo è il principale
meccanismo di controllo degli accessi di Go. Un tipo struct può contenere un misto di campi esportati e
non esportati.
I tipi di struttura tendono a essere prolissi, perché spesso comportano una riga per ogni campo. Anche
se si potrebbe scrivere l'intero tipo ogni volta che è necessario, la ripetizione diventerebbe noiosa. Invece,
i tipi struct di solito appaiono all'interno della dichiarazione di un tipo denominato, come Employee.
Una struct di tipo S non può dichiarare un campo dello stesso tipo S: un valore aggregato non può
contenere se stesso. (Ma S può dichiarare un campo del tipo puntatore *S, il che ci permette di creare
strutture di dati ricorsive come liste collegate e alberi. Ciò è illustrato nel codice seguente, che utilizza un
albero binario per implementare un ordinamento di inserimento:
gopl.io/ch4/treesort
tipo albero struct {
valore int
sinistra, destra
*albero
}

// Sort ordina i valori al loro


posto. func Sort(values []int) {
var radice *albero
per _, v := intervallo di valori {
radice = add(radice, v)
}
appendValues(values[:0], root)
}

// appendValues aggiunge gli elementi di t ai valori nell'ordine


// e restituisce la fetta risultante.
func appendValues(values []int, t *tree) []int { if t !=
nil {
valori = appendValues(valori, t.left) valori =
append(valori, t.value) valori =
appendValues(valori, t.right)
}
valori di ritorno
}

www.it-ebooks.info
102 CAPITOLO 4. TIPI DI COMPOSITI

func add(t *albero, valore int) *albero { if


t == nil {
// Equivale a restituire &albero{valore: valore}. t =
new(albero)
t.value = valore
return t
}
se valore < t.value {
t.left = add(t.left, valore)
} else {
t.right = add(t.right, valore)
}
restituire t
}

Il valore zero di una struct è composto dai valori zero di ciascuno dei suoi campi. Di solito è
auspicabile che il valore zero sia un valore predefinito naturale o sensato. Ad esempio, in bytes.Buffer, il
valore iniziale della struct è un buffer vuoto pronto all'uso e il valore zero di sync.Mutex, che vedremo nel
Capitolo 9, è un mutex sbloccato pronto all'uso. A volte questo comportamento iniziale sensato è
gratuito, ma a volte il progettista del tipo deve lavorarci su.
Il tipo struct senza campi è chiamato struct vuoto, scritto struct{}. Ha dimensione zero e non trasporta
informazioni, ma può essere comunque utile. Alcuni programmatori di Go lo usano al posto di bool
come tipo di valore di una mappa che rappresenta un insieme, per sottolineare che solo le chiavi sono
significative, ma il risparmio di spazio è marginale e la sintassi più macchinosa, quindi in genere lo
evitiamo.
seen := make(map[string]struct{}) // insieme di stringhe
// ...
if _, ok := seen[s]; !ok {
seen[s] = struct{}{}
// ... è la prima volta che vedo...
}

4.4.1. Strutture letterali

Un valore di un tipo struct può essere scritto utilizzando un letterale struct che specifica i valori dei suoi
campi.
tipo Punto struct{ X, Y int } p :=
Punto{1, 2}

Esistono due forme di struct literal. La prima forma, mostrata sopra, richiede che venga specificato un
valore per ogni campo, nel giusto ordine. Questo comporta l'onere per chi scrive (e per chi legge) di
ricordare esattamente quali sono i campi e rende il codice fragile nel caso in cui l'insieme dei campi
dovesse crescere o essere riordinato in seguito. Di conseguenza, questa forma tende a essere usata solo
all'interno del pacchetto che definisce il tipo di struct, o con tipi di struct più piccoli per i quali esiste
un'ovvia disposizione dei campi, come image.Point{x, y} o color.RGBA{rosso, verde, blu, alfa}.

www.it-ebooks.info
SEZIONE 4.4. STRUTTURE 103

Più spesso si usa la seconda forma, in cui il valore di una struct viene inizializzato elencando alcuni o
tutti i nomi dei campi e i loro valori corrispondenti, come in questa istruzione del programma Lissajous
della Sezione 1.4:
anim := gif.GIF{LoopCount: nframes}

Se un campo viene omesso in questo tipo di letterale, viene impostato al valore zero per il suo tipo.
Poiché i nomi sono forniti, l'ordine dei campi non ha importanza.
Le due forme non possono essere mescolate nello stesso letterale. Non si può nemmeno usare la prima
forma di letterale (basata sull'ordine) per aggirare la regola secondo cui gli identificatori non esportati
non possono essere riferiti a un altro pacchetto.
pacchetto p
tipo T struct{ a, b int } // a e b non sono esportati
pacchetto q
importare "p"
var _ = p.T{a: 1, b: 2} // errore di compilazione: impossibile fare
riferimento a, b var _ = p.T{1, 2} // errore di compilazione: impossibile
fare riferimento ad a, b

Sebbene l'ultima riga non menzioni gli identificatori di campo non esportati, in realtà li utilizza
implicitamente, quindi non è consentito.
I valori delle strutture possono essere passati come argomenti alle funzioni e restituiti da queste ultime.
Ad esempio, questa funzione scala un Punto di un fattore specificato:
func Scale(p Point, factor int) Point { restituisce
Point{p.X * factor, p.Y * factor}
}
fmt.Println(Scala(Punto{1, 2}, 5)) // "{5 10}"

Per efficienza, i tipi di struct più grandi vengono solitamente passati alle funzioni o restituiti da esse in
modo indiretto, utilizzando un puntatore,
func Bonus(e *Dipendente, percent int) int {
restituisce e.Salario * percent / 100
}

e questo è necessario se la funzione deve modificare il suo argomento, poiché in un linguaggio call-by-
value come Go, la funzione chiamata riceve solo una copia di un argomento, non un riferimento
all'argomento originale.
func AwardAnnualRaise(e *Employee) { e.Salary
= e.Salary * 105 / 100
}

Poiché le struct sono comunemente trattate tramite puntatori, è possibile utilizzare questa notazione
abbreviata per creare e inizializzare una variabile struct e ottenerne l'indirizzo:
pp := &Punto{1, 2}

È esattamente equivalente a

www.it-ebooks.info
104 CAPITOLO 4. TIPI DI COMPOSITI

pp := nuovo(Punto)
*pp = Punto{1, 2}

ma &Punto{1, 2} può essere usato direttamente all'interno di un'espressione, come una chiamata di
funzione.

4.4.2. Confronto tra strutture

Se tutti i campi di una struct sono confrontabili, la struct stessa è confrontabile, quindi due espressioni di
questo tipo possono essere confrontate usando == o !=. L'operazione == confronta i campi corrispondenti
delle due struct in ordine, quindi le due espressioni stampate qui sotto sono equivalenti:
tipo Punto struct{ X, Y int }

p := Punto{1, 2}
q := Punto{2, 1}
fmt.Println(p.X == q.X && p.Y == q.Y) // "falso"
fmt.Println(p == q) // "falso"

I tipi di struct comparabili, come altri tipi comparabili, possono essere utilizzati come tipo di chiave di una
mappa.
tipo indirizzo struct {
hostname string
port int
}

hits := make(map[indirizzo]int)
hits[indirizzo{"golang.org", 443}]++

4.4.3. Incorporamento di strutture e campi anonimi

In questa sezione vedremo come l'insolito meccanismo di incorporazione delle struct di Go ci permetta di
utilizzare un tipo di struct denominata come campo anonimo di un altro tipo di struct, fornendo una
comoda scorciatoia sintattica in modo che una semplice espressione di punti come x.f possa indicare
una catena di campi come x.d.e.f.

Consideriamo un programma di disegno 2D che fornisce una libreria di forme, come rettangoli, ellissi,
stelle e ruote. Ecco due dei tipi che potrebbe definire:
tipo Cerchio struct { X,
Y, Raggio int
}

tipo Ruota struct {


X, Y, Raggio, Raggi int
}

Un cerchio ha campi per le coordinate X e Y del suo centro e un raggio. Una ruota ha tutte le
caratteristiche di un cerchio, più i raggi, ovvero il numero di raggi inscritti. Creiamo una ruota:

www.it-ebooks.info
SEZIONE 4.4. STRUTTURE 105

var w Ruota
w.X = 8
w.Y = 8
w.Radius = 5
w.Raggi = 20

Man mano che l'insieme delle forme cresce, è inevitabile notare somiglianze e ripetizioni tra di esse,
quindi può essere conveniente fattorizzare le loro parti comuni:

tipo Punto struct { X, Y


int
}

type Circle struct {


Centro Punto
Raggio int
}

tipo Ruota struct {


Cerchio Cerchio
Raggi int
}

L'applicazione potrebbe essere più chiara, ma questa modifica rende l'accesso ai campi di una Ruota più
semplice.
più prolisso:

var w Wheel
w.Circle.Center.X = 8
w.Circle.Center.Y = 8
w.Circle.Radius = 5
w.Raggi = 20

Go consente di dichiarare un campo con un tipo ma senza nome; tali campi sono chiamati campi
anonimi. Il tipo del campo deve essere un tipo con nome o un puntatore a un tipo con nome. Di seguito,
Circle e Wheel hanno un campo anonimo ciascuno. Diciamo che un punto è incorporato in un
cerchio e un cerchio è incorporato in una ruota.

tipo Cerchio struct {


Punto
Raggio int
}

tipo Ruota struct {


Cerchio
Raggi int
}

Grazie all'incorporazione, possiamo fare riferimento ai nomi delle foglie dell'albero implicito senza fornire i
nomi intermedi:

www.it-ebooks.info
106 CAPITOLO 4. TIPI DI COMPOSITI

var w Ruota
w.X = 8 // equivalente a w .Circle.Point.X = 8
w.Y = 8 // equivalente a w.Circle.Point.Y = 8
w.Radius = 5 // equivalente a w.Circle.Radius = 5
w.Spokes = 20

Le forme esplicite mostrate nei commenti precedenti sono comunque valide, dimostrando che
"campo anonimo" è un termine improprio. I campi Cerchio e Punto hanno un nome, quello del tipo
chiamato, ma questi nomi sono facoltativi nelle espressioni a punti. Possiamo omettere uno o tutti i
campi anonimi quando selezioniamo i loro sottocampi.
Sfortunatamente, non esiste una stenografia corrispondente per la sintassi letterale della struct, quindi
nessuna delle due compila:
w = Ruota{8, 8, 5, 20} // errore di compilazione: campi
sconosciuti w = Ruota{X: 8, Y: 8, Raggio: 5, Raggi: 20} // errore di compilazione: campi
sconosciuti

Il letterale della struct deve seguire la forma della dichiarazione del tipo, quindi dobbiamo usare una
delle due forme seguenti, che sono equivalenti tra loro:
gopl.io/ch4/embed
w = Ruota{Circolo{Punto{8, 8}, 5}, 20}
w = Ruota{
Cerchio: Cerchio{
Punto: Punto{X: 8, Y: 8},
Raggio: 5,
},
Raggi: 20, // NOTA: la virgola finale è necessaria in questo punto (e in Raggio).
}
fmt.Printf("%#v\n", w )
// Uscita:
// Ruota{Circolo:Cerchio{Punto:Punto{X:8, Y:8}, Raggio:5}, Raggi:20}
w.X = 42
fmt.Printf("%#v\n", w )
// Uscita:
// Ruota{Circolo:Cerchio{Punto:Punto{X:42, Y:8}, Raggio:5}, Raggi:20}

Notate come l'avverbio # faccia sì che il verbo %v di Printf visualizzi i valori in una forma simile a Go
syn- tax. Per i valori struct, questa forma include il nome di ciascun campo.
Poiché i campi ''anonimi'' hanno nomi impliciti, non si possono avere due campi anonimi dello stesso
tipo, poiché i loro nomi sarebbero in conflitto. E poiché il nome del campo è implicitamente
determinato dal suo tipo, lo è anche la visibilità del campo. Negli esempi precedenti, i campi anonimi
Point e Circle sono esportati. Se fossero stati non esportati (punto e cerchio), avremmo potuto
comunque usare la forma abbreviata
w.X = 8 // equivalente a w.circle.point.X = 8

ma la forma lunga esplicita mostrata nel commento sarebbe proibita al di fuori del pacchetto
dichiarante, perché cerchio e punto sarebbero inaccessibili.

www.it-ebooks.info
SEZIONE 4.5. JSON 107

Quello che abbiamo visto finora dell'incorporazione di struct è solo una spolverata di zucchero sintattico
sulla notazione a punti usata per selezionare i campi struct. Più avanti vedremo che i campi anonimi non
devono necessariamente essere tipi struct; qualsiasi tipo chiamato o puntatore a un tipo chiamato andrà
bene. Ma perché si vorrebbe incorporare un tipo che non ha sottocampi?
La risposta ha a che fare con i metodi. La notazione abbreviata usata per selezionare i campi di un tipo
incorporato funziona anche per selezionare i suoi metodi. In effetti, il tipo struct esterno acquisisce non
solo i campi del tipo incorporato, ma anche i suoi metodi. Questo meccanismo è il modo principale in
cui i comportamenti degli oggetti complessi vengono composti a partire da quelli più semplici. La
composizione è un elemento centrale della programmazione orientata agli oggetti in Go, che verrà
approfondito nella Sezione 6.3.

4.5. JSON

JavaScript Object Notation (JSON) è una notazione standard per l'invio e la ricezione di informazioni
strutturate. JSON non è l'unica notazione di questo tipo. XML (§7.14), ASN.1 e i Protocol Buffers di
Google hanno scopi simili e ognuno ha la sua nicchia, ma per la sua semplicità, leggibilità e supporto
universale, JSON è il più usato.
Go dispone di un eccellente supporto per la codifica e la decodifica di questi formati, fornito dai
pacchetti di libreria standard encoding/json, encoding/xml, encoding/asn1 e così via, e questi
pacchetti hanno tutti API simili. Questa sezione fornisce una breve panoramica delle parti più
importanti del pacchetto encoding/json.
JSON è una codifica di valori JavaScript - stringhe, numeri, booleani, array e oggetti - come testo
Unicode. È una rappresentazione efficiente e leggibile per i tipi di dati di base del Capitolo 3 e per i tipi
compositi di questo capitolo: array, slices, struct e mappe.
I tipi di base di JSON sono i numeri (in notazione decimale o scientifica), i booleani (vero o
falso) e le stringhe, che sono sequenze di punti di codice Unicode racchiuse tra doppi apici, con escape
backslash che utilizzano una notazione simile a quella di Go, sebbene gli escape numerici di JSON
indichino codici UTF-16, non rune.
Questi tipi di base possono essere combinati in modo ricorsivo utilizzando array e oggetti JSON. Un
array JSON è una sequenza ordinata di valori, scritta come un elenco separato da virgole e racchiuso da
parentesi quadre; gli array JSON sono usati per codificare array e slices di Go. Un oggetto JSON è una
mappatura da stringhe a valori, scritta come una sequenza di coppie nome:valore separate da virgole e
arrotondate da parentesi graffe; gli oggetti JSON sono usati per codificare mappe Go (con chiavi stringa)
e strutture. Ad esempio:
booleano vero
numero -273.15
stringa "Ha detto "Ciao, B$"".
array ["oro", "argento", "bronzo"].
oggetto {"anno": 1980,
"evento": "tiro con
l'arco",
"medaglie": ["oro", "argento", "bronzo"]}.

www.it-ebooks.info
108 CAPITOLO 4. TIPI DI COMPOSITI

Consideriamo un'applicazione che raccoglie recensioni di film e offre raccomandazioni. Il tipo di dati
Movie e un tipico elenco di valori sono dichiarati di seguito. (I letterali di stringa dopo le dichiarazioni
dei campi Anno e Colore sono tag di campo; li spiegheremo tra poco).
gopl.io/ch4/movie
tipo Film struct {
Titolo stringa
Anno int `json: "rilasciato"`
Colore bool `json: "color,omitempty" `
Attori []stringa
}

var movies = []Movie{


{Titolo: "Casablanca", Anno: 1942, Colore: falso,
Attori: []string{"Humphrey Bogart", "Ingrid Bergman"}},
{Titolo: "Cool Hand Luke", Anno: 1967, Colore: true, Attori:
[]string{"Paul Newman"}},
{Titolo: "Bullitt", Anno: 1968, Colore: vero,
Attori: []string{"Steve McQueen", "Jacqueline Bisset"}},
// ...
}

Strutture di dati come questa si adattano perfettamente a JSON ed è facile convertirle in entrambe
le direzioni. La conversione di una struttura di dati Go come i filmati in JSON si chiama marshalling.
Il marshalling viene eseguito da json.Marshal:
data, err := json.Marshal(movies) if
err := nil {
log.Fatalf("JSON marshaling non riuscito: %s", err)
}
fmt.Printf("%s\n", dati)

Marshal produce un byte slice contenente una stringa molto lunga senza spazi bianchi estranei; abbiamo
piegato le righe in modo che si adattino:
[{"Titolo": "Casablanca", "uscita":1942, "Attori":["Humphrey Bogart", "Ingr id
Bergman"]},{"Titolo": "Cool Hand Luke", "uscita":1967, "colore":vero, "Attori":["Paul
Newman"]},{"Titolo": "Bullitt", "uscita":1968, "colore":vero, "Attori":["Steve
McQueen", "Jacqueline Bisset"]}]

Questa rappresentazione compatta contiene tutte le informazioni, ma è difficile da leggere. Per il


consumo umano, una variante chiamata json.MarshalIndent produce un output ben rientrato. Due
argomenti aggiuntivi definiscono un prefisso per ogni riga di output e una stringa per ogni livello di
rientro:
data, err := json.MarshalIndent(movies, "", " ")
if err : = nil {
log.Fatalf("JSON marshaling fallito: %s", err)
}
fmt.Printf("%s\n", dati)

www.it-ebooks.info
SEZIONE 4.5. JSON 109

Il codice qui sopra stampa


[
{
"Titolo": "Casablanca",
"uscita": 1942, "Attori": [
"Humphrey Bogart",
"Ingrid Bergman".
]
},
{
"Titolo": "Cool Hand Luke",
"rilasciato": 1967,
"colore": vero,
"Attori": [
"Paul Newman"
]
},
{
"Titolo": "Bullitt",
"uscita": 1968, "colore":
true,
"Attori": [
"Steve McQueen", "Jacqueline
Bisset"
]
}
]

Il marshalling utilizza i nomi dei campi della struttura Go come nomi dei campi per gli oggetti JSON
(attraverso la riflessione, come vedremo nella Sezione 12.6). Solo i campi esportati sono sottoposti a
marshalling, motivo per cui abbiamo scelto i nomi maiuscoli per tutti i nomi dei campi Go.
Si sarà notato che il nome del campo Anno è cambiato in rilasciato nell'output e che Colore è cambiato
in colore. Questo è dovuto ai tag di campo. Un tag di campo è una stringa di metadati associata in
fase di compilazione al campo di una struct:
Anno int `json: "rilasciato"`
Colore bool `json: "color,omitempty" `

Un tag di campo può essere una qualsiasi stringa letterale, ma viene convenzionalmente interpretato
come un elenco separato da spazi di coppie chiave: "valore"; poiché contengono doppie virgolette, i
tag di campo sono solitamente scritti con letterali di stringa grezzi. La chiave json controlla il
comportamento del pacchetto encoding/json e gli altri pacchetti encoding/... seguono questa
convenzione. La prima parte del tag json field specifica un nome JSON alternativo per il campo
Go. I tag field sono spesso usati per specificare un nome JSON idiomatico come total_count per un
campo Go chiamato TotalCount. Il tag per Color ha un'opzione aggiuntiva, omitempty, che indica che non
deve essere prodotto alcun output JSON se il campo ha il valore zero per il suo tipo (false, in
questo caso) o è altrimenti vuoto. Sicuramente, l'output JSON per Casablanca, un film in bianco e
nero, non ha un campo colore.

www.it-ebooks.info
110 CAPITOLO 4. TIPI DI COMPOSITI

L'operazione inversa al marshalling, ossia la decodifica di JSON e il popolamento di una struttura dati
Go, si chiama unmarshaling e viene eseguita da json.Unmarshal. Il codice sottostante scompone i
dati del filmato JSON in una fetta di strutture il cui unico campo è il titolo. Definendo strutture di dati
Go adeguate in questo modo, è possibile selezionare quali parti dell'input JSON decodificare e quali dis-
cartellare. Quando Unmarshal ritorna, ha riempito la slice con le informazioni sul titolo; gli altri
nomi nel JSON vengono ignorati.
var titles []struct{ Titolo stringa }
if err := json.Unmarshal(data, &titoli); err := nil {
log.Fatalf("JSON unmarshaling failed: %s", err)
}
fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]"

Molti servizi web forniscono un'interfaccia JSON: si fa una richiesta con HTTP e si ottengono le
informazioni desiderate in formato JSON. Per illustrare, interroghiamo l'issue tracker di GitHub
utilizzando l'interfaccia del servizio web. Per prima cosa definiamo i tipi e le costanti necessarie:
gopl.io/ch4/github
// Il pacchetto github fornisce un'API Go per il tracker dei problemi di GitHub.
// Vedere https://developer.github.com/v3/search/#search-issues.
pacchetto github
importare "tempo"

const IssuesURL = "https://api.github.com/search/issues" type

IssuesSearchResult struct {
TotalCount int `json: "total_count" `
Articoli []*Issue
}
tipo Issue struct {
Number int
HTMLURL stringa `json: "html_url"`
Titolo stringa
Stato stringa
Utente *Utente
CreatedAt time.Time `json: "created_at"`
Corpo stringa // in formato Markdown
}
tipo Utente struct {
Login stringa
HTMLURL stringa `json: "html_url"`
}

Come in precedenza, i nomi di tutti i campi delle struct devono essere maiuscoli anche se i loro nomi
JSON non lo sono. Tuttavia, il processo di corrispondenza che associa i nomi JSON con i nomi delle
strutture Go durante l'unmarshaling è insensibile alle maiuscole e alle minuscole, quindi è necessario
usare un tag di campo solo quando c'è una minuscola nel nome JSON ma non nel nome Go. Anche in
questo caso, siamo selettivi sui campi da decodificare; la risposta alla ricerca su GitHub contiene molte
più informazioni di quelle mostrate qui.

www.it-ebooks.info
SEZIONE 4.5. JSON 111

La funzione SearchIssues effettua una richiesta HTTP e decodifica il risultato come JSON. Poiché i
termini della query presentati dall'utente potrebbero contenere caratteri come ? e & che hanno un
significato speciale in un URL, utilizziamo url.QueryEscape per garantire che vengano presi alla
lettera.
gopl.io/ch4/github
pacchetto github

importare (
"encoding/json"
"fmt" "net/http"
"net/url" "strings"
)

// SearchIssues interroga il tracker dei problemi di GitHub.


func SearchIssues(terms []string) (*IssuesSearchResult, error) { q :=
url.QueryEscape(strings.Join(terms, " "))
resp, err := http.Get(IssuesURL + "?q=" + q) if err
!= nil {
restituire nil, err
}

// Dobbiamo chiudere resp.Body su tutti i percorsi di esecuzione.


// (il capitolo 5 presenta la funzione "defer", che semplifica
questa operazione) if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("search query failed: %s", resp.Status)
}

var risultato Risultati della ricerca


if err := json.NewDecoder(resp.Body).Decode(&result); err := nil { resp.Body.Close()
restituire nil, err
}
resp.Body.Close()
return &result, nil
}

Gli esempi precedenti hanno utilizzato json.Unmarshal per decodificare l'intero contenuto di una
fetta di byte come una singola entità JSON. Per varietà, questo esempio utilizza il decodificatore di
streaming, json.Decoder, che consente di decodificare diverse entità JSON in sequenza dallo stesso
flusso, anche se in questo caso non ne abbiamo bisogno. Come ci si potrebbe aspettare, esiste un
codificatore di streaming corrispondente, chiamato json.Encoder.

La chiamata a Decode popola la variabile result. Ci sono vari modi per formattare bene il suo valore. Il
più semplice, dimostrato dal comando issues qui sotto, è una tabella di testo con colonne a larghezza
fissa, ma nella prossima sezione vedremo un approccio più sofisticato basato sui modelli.

www.it-ebooks.info
112 CAPITOLO 4. TIPI DI COMPOSITI

gopl.io/ch4/problemi
// Issues stampa una tabella di problemi GitHub che corrispondono ai termini di
ricerca. pacchetto main
importare (
"fmt"
"log"
"os"
"gopl.io/ch4/github"
)
func main() {
result, err := github.SearchIssues(os.Args[1:]) if err
:= nil {
log.Fatal(err)
}
fmt.Printf("%d issues:\n", result.TotalCount) for _,
item := range result.Items {
fmt.Printf("#%-5d %9.9s %.55s\n", item.Number,
item.User.Login, item.Title)
}
}

Gli argomenti della riga di comando specificano i termini di ricerca. Il comando seguente interroga l'issue
tracker del progetto Go per trovare l'elenco dei bug aperti relativi alla decodifica JSON:
$ go build gopl.io/ch4/issues
$ ./issues repo:golang/go is:open json decoder
13 numeri:
#5680 eaigner codifica/json: impostare il convertitore di chiavi su
en/decoder #6050 gopherbot codifica/json: fornire il tokenizer
#8658 codifica gopherbot/json: usare bufio
#8462 codifica kortschak/json: UnmarshalText confonde json.Unmarshal #5901
rsc encoding/json: consentire l'override del tipo di marshaling
#9812 klauspost encoding/json: tag stringa non simmetrico
#7872 codifica extempora/json: Il codificatore bufferizza internamente l'output
completo #9650 cespare codifica/json: La decodifica dà errPhase quando si
smarca #6716 gopherbot encoding/json: includere il nome del campo nell'errore di
smarcamento me #6901 lukescott encoding/json, encoding/xml: opzione per trattare i fi
sconosciuti #6384 joeshaw encoding/json: codificare numeri interi precisi in virgola
mobile u #6647 btracey x/tools/cmd/godoc: visualizzare il tipo di ogni t i p o nominato
#4237 gjemiller encoding/base64: Il padding di URLEncoding è facoltativo

L'interfaccia del servizio web di GitHub all'indirizzo https://developer.github.com/v3/ ha molte più


funzioni di quelle che abbiamo a disposizione in questa sede.
Esercizio 4.10: Modificare i problemi per riportare i risultati in categorie di età, ad esempio meno di un
mese, meno di un anno e più di un anno.
Esercizio 4.11: Creare uno strumento che consenta agli utenti di creare, leggere, aggiornare e cancellare i
problemi di GitHub dalla riga di comando, richiamando l'editor di testo preferito quando è richiesto un
input di testo sostanziale.

www.it-ebooks.info
SEZIONE 4.6. MODELLI DI TESTO E HTML 113

Esercizio 4.12: Il popolare fumetto web xkcd ha un'interfaccia JSON. Per esempio, una richiesta a
https://xkcd.com/571/info.0.json produce una descrizione dettagliata del fumetto 571, uno dei
tanti preferiti. Scaricare ogni URL (una volta!) e costruire un indice offline. Scrivete uno strumento xkcd
che, utilizzando questo indice, stampi l'URL e la trascrizione di ogni fumetto che corrisponde a un
termine di ricerca fornito sulla riga di comando.
Esercizio 4.13: Il servizio web basato su JSON dell'Open Movie Database consente di cercare un film per
nome all'indirizzo https://omdbapi.com/ e di scaricarne l'immagine della locandina. Scrivete uno strumento
che scarichi l'immagine della locandina del film indicato alla riga di comando.

4.6. Modelli di testo e HTML

L'esempio precedente esegue solo la formattazione più semplice possibile, per la quale Printf è del tutto
adeguata. Ma a volte la formattazione deve essere più elaborata ed è auspicabile separare il formato dal
codice in modo più completo. Questo può essere fatto con i pacchetti text/template e
html/template, che forniscono un meccanismo per sostituire i valori delle variabili in un modello di
testo o HTML.
Un modello è una stringa o un file contenente una o più parti racchiuse tra doppie parentesi,
{{...}}, chiamati azioni. La maggior parte della stringa viene stampata letteralmente, ma le azioni
attivano altri comportamenti. Ogni azione contiene un'espressione nel linguaggio dei modelli, una
notazione semplice ma potente per la stampa di valori, la selezione di campi struct, la chiamata di
funzioni e metodi, l'espressione di flussi di controllo come istruzioni if-else e cicli di range e
l'istanziazione di altri modelli. Di seguito viene mostrata una semplice stringa di template:
gopl.io/ch4/issuesreport
const templ = `{{.TotalCount}} problemi:
{{range .Items}}----------------------------------------
Numero: {{.Number}}
Utente: {{.User.Login}}
Titolo: {{.Titolo | printf "%.64s" }} Età:
{{.CreatedAt | daysAgo}} giorni
{{fine}}`

Questo modello stampa prima il numero di problemi corrispondenti, poi il numero, l'utente, il titolo e
l'età in giorni di ciascuno di essi. All'interno di un'azione, c'è una nozione di valore corrente, denominata
"punto" e scritta come ''.'', un punto. Il punto si riferisce inizialmente al parametro del template, che in
questo esempio sarà un github.IssuesSearchResult. L'azione {{.TotalCount}} si espande al valore
del campo TotalCount, stampato nel m o d o consueto. L'azione
Le azioni {{range .Items}} e {{end}} creano un ciclo, quindi il testo tra di esse viene espanso più
volte, con il punto legato agli elementi successivi degli Items.
All'interno di un'azione, la notazione | rende il risultato di un'operazione l'argomento di un'altra, in
modo analogo a una pipeline della shell Unix. Nel caso del Titolo, la seconda operazione è la
funzione printf, che è un sinonimo incorporato di fmt.Sprintf in tutti i modelli. Nel caso di Age, la
seconda operazione è la seguente funzione, daysAgo, che converte il campo CreatedAt in un campo

www.it-ebooks.info
114 CAPITOLO 4. TIPI DI COMPOSITI

tempo trascorso, utilizzando time.Since:


func daysAgo(t time.Time) int {
restituire int(time.Since(t).Hours() / 24)
}

Si noti che il tipo di CreatedAt è time.Time, non string. Allo stesso modo in cui un tipo può controllare la
sua formattazione delle stringhe (§2.5) definendo alcuni metodi, un tipo può anche definire dei metodi
per controllare il suo comportamento di marshalling e unmarshaling JSON. Il valore JSON
marshallizzato di time.Time è una stringa in un formato standard.

La produzione di output con un modello è un processo in due fasi. Prima si deve analizzare il modello in
una rappresentazione interna adeguata e poi eseguirlo su input specifici. Il parsing deve essere fatto una
sola volta. Il codice qui sotto crea e analizza il template templ definito in precedenza. Si noti la
concatenazione delle chiamate ai metodi: template.New crea e restituisce un modello; Funcs aggiunge
daysAgo all'insieme di funzioni accessibili all'interno di questo modello, quindi restituisce tale modello;
infine, Parse viene richiamato sul risultato.
report, err := template.New("report").
Funcs(template.FuncMap{"daysAgo": daysAgo}). Parse(templ)
if err != nil {
log.Fatal(err)
}

Poiché i template sono solitamente fissati in fase di compilazione, il fallimento dell'analisi di un template
indica un bug fatale nel programma. La funzione helper template.Must rende la gestione degli errori più
semplice: accetta un modello e un errore, controlla che l'errore sia nullo (e in caso contrario va in
panico) e poi restituisce il modello. Torneremo su questa idea nella Sezione 5.9.

Una volta che il modello è stato creato, aumentato con daysAgo, analizzato e controllato, possiamo
eseguirlo usando github.IssuesSearchResult come fonte di dati e os.Stdout come destinazione:
var report = template.Must(template.New("issuelist").
Funcs(template.FuncMap{"daysAgo": daysAgo}). Parse(templ))

func main() {
result, err := github.SearchIssues(os.Args[1:]) if err
:= nil {
log.Fatal(err)
}
if err := report.Execute(os.Stdout, result); err := nil { log.Fatal(err)
}
}

Il programma stampa un rapporto di testo semplice come questo:

www.it-ebooks.info
SEZIONE 4.6. MODELLI DI TESTO E HTML 115

$ go build gopl.io/ch4/issuesreport
$ ./issuesreport repo:golang/go is:open json decoder
13 numeri:

Numero: 5680
Utente: eaigner
Titolo: codifica/json: impostare convertitore chiave su
en/decoder Età: 750 giorni

Numero: 6050 Utente:


gopherbot
Titolo: codifica/json: fornire tokenizer Età:
695 giorni

...

Passiamo ora al pacchetto html/template. Utilizza la stessa API e lo stesso linguaggio di espressione
di text/template, ma aggiunge funzioni per l'escape automatico e appropriato al contesto delle
stringhe che compaiono in HTML, JavaScript, CSS o URL. Queste funzioni possono aiutare a evitare un
problema di sicurezza perenne della generazione di HTML, un attacco di tipo injection, in cui un
avversario crea un valore stringa, come il titolo di un argomento, per includere codice dannoso che, se
non correttamente evaso da un template, gli dà il controllo della pagina.

Il modello sottostante stampa l'elenco dei problemi come tabella HTML. Si noti la diversa importazione:

gopl.io/ch4/issueshtml
importare "html/template"

var issueList = template.Must(template.New("issuelist").Parse(`


<h1>{{.TotalCount}} problemi</h1>
<tabella>
<tr style='text-align: left'>
<th>#</th>
<th>Stato</th>
<th>Utente</th>
<th>Titolo</th>
</tr>.
{{range .Items}}
<tr>
<td><a href='{{.HTMLURL}}'>{{{.Number}}</td>
<td>{{.Stato}}</td>
<td><a href='{{.User.HTMLURL}}'>{{.User.Login}}</a></td>
<td><a href='{{.HTMLURL}}'>{{{.Title}}</a></td>
</tr>.
{{fine}}
</tab>.
`))

Il comando seguente esegue il nuovo modello sui risultati di una query leggermente diversa:

www.it-ebooks.info
116 CAPITOLO 4. TIPI DI COMPOSITI

$ go build gopl.io/ch4/issueshtml
$ ./issueshtml repo:golang/go commenter:gopherbot json encoder >issues.html

La Figura 4.4 mostra l'aspetto della tabella in un browser web. I link rimandano alle pagine web
appropriate di GitHub.

Figura 4.4. Tabella HTML dei problemi del progetto Go relativi alla codifica JSON.

Nessuno dei problemi della Figura 4.4 rappresenta una sfida per l'HTML, ma possiamo vedere l'effetto
più chiaramente con i problemi i cui titoli contengono metacaratteri HTML come & e <. Per questo
esempio abbiamo scelto due problemi di questo tipo:

$ ./issueshtml repo:golang/go 3133 10535 >issues2.html

La Figura 4.5 mostra il risultato di questa query. Si noti che il pacchetto html/template esegue
automaticamente l'escape HTML dei titoli, in modo che appaiano letteralmente. Se avessimo usato per
errore il pacchetto text/template, la stringa di quattro caratteri "&lt;" sarebbe stata resa come un carattere
meno di '<' e la stringa "<link>" sarebbe diventata un elemento di collegamento, cambiando la
struttura del documento HTML e forse compromettendone la sicurezza.

È possibile sopprimere questo comportamento di auto-escaping per i campi che contengono dati HTML
attendibili, utilizzando il tipo di stringa denominata template.HTML al posto di string. Esistono tipi
denominati simili per JavaScript, CSS e URL affidabili. Il programma seguente dimostra il principio
utilizzando due campi con lo stesso valore ma di tipo diverso: A è una stringa e B è un template.HTML.

www.it-ebooks.info
SEZIONE 4.6. MODELLI DI TESTO E HTML 117

Figura 4.5. I metacaratteri HTML nei titoli dei numeri sono visualizzati correttamente.

gopl.io/ch4/autoescape
func main() {
const templ = `<p>A: {{.A}}</p><p>B: {{.B}}</p>`
t := template.Must(template.New("escape").Parse(templ)) var
data struct {
Una stringa // testo normale non attendibile
B template.HTML // HTML affidabile
}
data.A = "<b>Ciao!</b>" data.B =
"<b>Ciao!</b>"
if err := t.Execute(os.Stdout, data); err := nil {
log.Fatal(err)
}
}

La Figura 4.6 mostra l'output del modello come appare in un browser. Si può notare che A è stato sottoposto
all'escape, mentre B no.

Figura 4.6. I valori delle stringhe sono caratterizzati dall'escape HTML, mentre i valori template.HTML
non lo sono.
Qui abbiamo spazio per mostrare solo le caratteristiche di base del sistema di template. Come sempre, per
ulteriori informazioni, consultare la documentazione del pacchetto:
$ go doc text/template
$ go doc html/template

Esercizio 4.14: Creare un server web che interroghi GitHub una volta e poi consenta la navigazione
nell'elenco delle segnalazioni di bug, delle pietre miliari e degli utenti.

www.it-ebooks.info
Questa pagina è stata lasciata intenzionalmente in bianco

www.it-ebooks.info
5
Funzioni

Una funzione ci permette di impacchettare una sequenza di istruzioni come un'unità che può essere
richiamata da un'altra parte del programma, magari più volte. Le funzioni permettono di suddividere un
grosso lavoro in pezzi più piccoli, che potrebbero essere scritti da persone diverse, separate sia dal tempo
che dallo spazio. Una funzione nasconde i dettagli dell'implementazione agli utenti. Per tutti questi
motivi, le funzioni sono una parte fondamentale di qualsiasi linguaggio di programmazione.
Abbiamo già visto molte funzioni. Ora ci prendiamo un po' di tempo per una discussione più
approfondita. L'esempio di questo capitolo è un web crawler, cioè il componente di un motore di ricerca
web responsabile dell'acquisizione delle pagine web, della scoperta dei collegamenti al loro interno,
dell'acquisizione delle pagine identificate da tali collegamenti e così via. Un web crawler ci offre
un'ampia opportunità di esplorare la ricorsione, le funzioni anonime, la gestione degli errori e gli aspetti
delle funzioni che sono propri di Go.

5.1. Funzione Dichiarazioni

Una dichiarazione di funzione ha un nome, un elenco di parametri, un elenco opzionale di risultati e un


corpo:
func name(parameter-list) (result-list) {
corpo
}

L'elenco dei parametri specifica i nomi e i tipi dei parametri della funzione, ovvero le variabili locali i cui
valori o argomenti sono forniti dal chiamante. L'elenco dei risultati specifica i tipi di valori che la
funzione restituisce. Se la funzione restituisce un risultato senza nome o nessun risultato, le parentesi
sono facoltative e di solito vengono omesse. Se si omette completamente l'elenco dei risultati, si dichiara
una funzione che non restituisce alcun valore e che viene chiamata solo per i suoi effetti. Nella funzione
hypot,

119

www.it-ebooks.info
120 CAPITOLO 5. FUNZIONI

func hypot(x, y float64) float64 { return


math.Sqrt(x*x + y*y)
}
fmt.Println(ipot(3, 4)) // "5"

x e y sono parametri nella dichiarazione, 3 e 4 sono argomenti della chiamata e la funzione restituisce
un valore float64.
Come i parametri, i risultati possono essere denominati. In questo caso, ogni nome dichiara una
variabile locale ini- zializzata al valore zero per il suo tipo.
Una funzione che ha un elenco di risultati deve terminare con una dichiarazione di ritorno, a meno che
l'esecuzione non possa chiaramente raggiungere la fine della funzione, magari perché la funzione
termina con una chiamata al panico o con un ciclo for infinito senza interruzione.
Come abbiamo visto con hypot, una sequenza di parametri o risultati dello stesso tipo può essere
fattorizzata in modo che il tipo stesso venga scritto una sola volta. Queste due dichiarazioni sono
equivalenti:
func f(i, j, k int, s, t string) { /* ... */ }
func f(i int, j int, k int, s string, t string) { /* ... */ }

Ecco quattro modi per dichiarare una funzione con due parametri e un risultato, tutti di tipo int.
L'identificatore vuoto può essere usato per sottolineare che un parametro è inutilizzato.
func add(x int, y int) int { restituisce x + y } func
sub(x, y int) (z int) { z = x - y; return }
func first(x int, _ int) int { return x }
func zero(int, int) int { return 0 }
fmt.Printf("%T\n", add) // "func(int, int) int"
fmt.Printf("%T\n", sub) // "func(int, int) int"
fmt.Printf("%T\n", first) // "func(int, int) int"
fmt.Printf("%T\n", zero) // "func(int, int) int"

Il tipo di una funzione viene talvolta chiamato firma. Due funzioni hanno lo stesso tipo o firma se hanno
la stessa sequenza di tipi di parametri e la stessa sequenza di tipi di risultati. I nomi dei parametri e dei
risultati non influiscono sul tipo, così come il fatto che siano stati dichiarati o meno con la forma
fattorizzata.
Ogni chiamata di funzione deve fornire un argomento per ogni parametro, nell'ordine in cui i parametri
sono stati dichiarati. Go non ha un concetto di valori predefiniti dei parametri, né un modo per
specificare gli argomenti per nome, quindi i nomi dei parametri e dei risultati non hanno importanza
per il chiamante, se non come documentazione.
I parametri sono variabili locali all'interno del corpo della funzione, il cui valore iniziale è impostato
sugli argomenti forniti dal chiamante. I parametri della funzione e i risultati nominati sono variabili
nello stesso blocco lessicale delle variabili locali più esterne della funzione.
Gli argomenti vengono passati per valore, quindi la funzione riceve una copia di ogni argomento; le
modifiche alla copia non hanno effetto sul chiamante. Tuttavia, se l'argomento contiene un qualche tipo
di riferimento, come un puntatore, una slice, una mappa, una funzione o un canale, il chiamante può
essere influenzato da qualsiasi modifica apportata dalla funzione alle variabili a cui l'argomento fa
indirettamente riferimento.

www.it-ebooks.info
SEZIONE 5.2. RICORSO 121

Può capitare di incontrare una dichiarazione di funzione senza corpo, a indicare che la funzione è
implementata in un linguaggio diverso da Go. Tale dichiarazione definisce la firma della funzione.
pacchetto matematica

func Sin(x float64) float64 // implementato in linguaggio assembly

5.2. Ricorsione

Le funzioni possono essere ricorsive, cioè possono richiamare se stesse, direttamente o indirettamente. La
ricorsione è una tecnica potente per molti problemi e naturalmente è essenziale per elaborare strutture di
dati ricorsive. Nella Sezione 4.4 abbiamo usato la ricorsione su un albero per implementare un semplice
ordinamento di inserimento. In questa sezione la utilizzeremo nuovamente per elaborare documenti
HTML.

Il programma di esempio qui sotto utilizza un pacchetto non standard, golang.org/x/net/html, che
fornisce un parser HTML. I repository golang.org/x/... contengono pacchetti progettati e mantenuti dal
team di Go per applicazioni come la rete, l'elaborazione di testo internazionalizzato, le piattaforme
mobili, la manipolazione delle immagini, la crittografia e gli strumenti per gli sviluppatori. Questi
pacchetti non sono presenti nella libreria standard perché sono ancora in fase di sviluppo o perché sono
raramente necessari alla maggior parte dei programmatori Go.

Le parti dell'API golang.org/x/net/html di cui avremo bisogno sono mostrate di seguito. La funzione
html.Parse legge una sequenza di byte, li analizza e restituisce la radice dell'albero dei documenti HTML,
che è un html.Node. L'HTML ha diversi tipi di nodi: testo, commenti e così via, ma qui ci interessano solo
i nodi di elementi della forma <nome-chiave='valore'>.
golang.org/x/net/html
pacchetto html

tipo Nodo struct {


Tipo Tipo di nodo
Dati stringa
Attr []Attributo
PrimoFiglio, ProssimoFratello *Nodo
}

tipo NodeType int32

const (
Nodo di errore NodeType = iota
TextNode
DocumentNode
ElementNode
CommentNode
DoctypeNode
)

www.it-ebooks.info
122 CAPITOLO 5. FUNZIONI

tipo Attributo struct {


Chiave, Val stringa
}

func Parse(r io.Reader) (*Node, error)

La funzione principale analizza l'input standard come HTML, estrae i collegamenti utilizzando un metodo
ricorsivo
e stampa ogni collegamento scoperto:
gopl.io/ch5/findlinks1
// Findlinks1 stampa i collegamenti in un documento HTML letto da input standard. pacchetto
main

importare (
"fmt"
"os"

"golang.org/x/net/html"
)

func main() {
doc, err := html.Parse(os.Stdin) if
err := nil {
fmt.Fprintf(os.Stderr, "findlinks1: %v\n", err)
os.Exit(1)
}
for _, link := range visit(nil, doc) {
fmt.Println(link)
}
}

La funzione visit attraversa un albero di nodi HTML, estrae il collegamento dall'attributo href di ogni
elemento di ancoraggio <a href='...'>, aggiunge i collegamenti a una slice di stringhe e restituisce
la slice risultante:
// visit aggiunge ai link ogni link trovato in n e restituisce il risultato. func
visit(links []string, n *html.Node) []string {
if n.Type == html.ElementNode && n.Data == "a" { for _, a
:= range n.Attr {
se a.Key == "href" {
link = append(link, a.Val)
}
}
}
per c := n.FirstChild; c := nil; c = c.NextSibling { link =
visit(links, c)
}
link di ritorno
}

Per discendere l'albero per un nodo n, visit richiama ricorsivamente se stesso per ciascuno dei figli
di n, che sono contenuti nell'elenco collegato FirstChild.

www.it-ebooks.info
SEZIONE 5.2. RICORSO 123

Eseguiamo findlinks sulla home page di Go, inviando l'output di fetch (§1.5) all'input di
findlinks. Abbiamo modificato leggermente l'output per brevità.
$ go build gopl.io/ch1/fetch
$ go build gopl.io/ch5/findlinks1
$ ./fetch https://golang.org | ./findlinks1 #
/doc/
/pkg/
/aiuto/
/blog/
http://play.golang.org/
//tour.golang.org/
https://golang.org/dl/
//blog.golang.org/
/LICENZA
/doc/tos.html http://www.google.com/intl/en/policies/privacy/

Si noti la varietà di forme di link che appaiono nella pagina. Più avanti vedremo come risolverli
rispetto all'URL di base, https://golang.org, per creare URL assoluti.
Il programma successivo utilizza la ricorsione sull'albero dei nodi HTML per stampare la struttura
dell'albero in forma schematica. Quando incontra ogni elemento, spinge il tag dell'elemento in una pila,
quindi stampa la pila.
gopl.io/ch5/outline
func main() {
doc, err := html.Parse(os.Stdin) if
err := nil {
fmt.Fprintf(os.Stderr, "outline: %v\n", err)
os.Exit(1)
}
outline(nil, doc)
}
func outline(stack []string, n *html.Node) { if
n.Type == html.ElementNode {
stack = append(stack, n.Data) // push tag
fmt.Println(stack)
}
per c := n.FirstChild; c := nil; c = c.NextSibling { outline(stack,
c)
}
}

Si noti una sottigliezza: sebbene outline ''spinga'' un elemento sulla pila, non c'è un corrispondente pop.
Quando outline chiama se stesso in modo ricorsivo, il chiamante riceve una copia dello stack. Sebbene il
chiamante possa aggiungere elementi a questa fetta, modificando l'array sottostante e forse anche
allocando un nuovo array, non modifica gli elementi iniziali che sono visibili al chiamante, quindi
quando la funzione ritorna, lo stack del chiamante è come era prima della chiamata.

www.it-ebooks.info
124 CAPITOLO 5. FUNZIONI

Ecco lo schema di https://golang.org, ancora una volta modificato per brevità:


$ go build gopl.io/ch5/outline
$ ./fetch https://golang.org | ./outline [html]
[html head] [html
head meta]
[titolo html] [link
html] [corpo html]
[html body div] [html
body div] [html body
div div]
[html body div div form] [html
body div div form div]
[html body div div form div a]
...

Come si può vedere sperimentando l'outline, la maggior parte dei documenti HTML può essere
elaborata con pochi livelli di ricorsione, ma non è difficile costruire pagine web patologiche che
richiedono una ricorsione estremamente profonda.
Molte implementazioni di linguaggi di programmazione utilizzano uno stack di chiamate di funzione a
dimensione fissa; le dimensioni tipiche vanno da 64KB a 2MB. Gli stack a dimensione fissa impongono
un limite alla profondità della ricorsione, quindi bisogna fare attenzione a evitare un overflow dello stack
quando si attraversano strutture di dati di grandi dimensioni in modo ricorsivo; gli stack a dimensione
fissa possono anche rappresentare un rischio per la sicurezza. Al contrario, le tipiche implementazioni di
Go utilizzano stack di dimensioni variabili che iniziano piccoli e crescono secondo le necessità fino a un
limite dell'ordine di un gigabyte. Questo ci permette di usare la ricorsione in modo sicuro e senza
preoccuparci dell'overflow.
Esercizio 5.1: Modificare il programma findlinks per attraversare l'elenco collegato n.FirstChild
utilizzando chiamate ricorsive a visit invece di un ciclo.
Esercizio 5.2: Scrivere una funzione per creare una mappatura tra i nomi degli elementi - p, div, span e
così via - e il numero di elementi con quel nome nell'albero di un documento HTML.
Esercizio 5.3: Scrivete una funzione per stampare il contenuto di tutti i nodi di testo nell'albero di un
documento HTML. Non scendete negli elementi <script> o <style>, poiché il loro contenuto non è
visibile in un browser web.
Esercizio 5.4: Estendere la funzione visit in modo che estragga altri tipi di collegamenti dal documento,
come immagini, script e fogli di stile.

5.3. Valori di ritorno multipli

Una funzione può restituire più di un risultato. Abbiamo visto molti esempi di funzioni di pacchetti
standard che restituiscono due valori, il risultato di calcolo desiderato e un valore di errore o un
booleano che indica se il calcolo ha funzionato. Il prossimo esempio mostra come scriverne una propria.

www.it-ebooks.info
SEZIONE 5.3. VALORI DI RITORNO MULTIPLI 125

Il programma che segue è una variante di findlinks che effettua direttamente la richiesta HTTP, in
modo da non dover più eseguire fetch. Poiché le operazioni HTTP e di parsing possono fallire,
findLinks dichiara due risultati: l'elenco dei collegamenti scoperti e un errore. Tra l'altro, il parser
HTML è solitamente in grado di recuperare da un input errato e di costruire un documento
contenente i nodi di errore, quindi Parse fallisce raramente; quando succede, è tipicamente dovuto a
errori di I/O sottostanti.
gopl.io/ch5/findlinks2
func main() {
for _, url := range os.Args[1:] {
link, err := findLinks(url) if err
:= nil {
fmt.Fprintf(os.Stderr, "findlinks2: %v\n", err) continue
}
per _, link := intervallo di link {
fmt.Println(link)
}
}
}

// findLinks esegue una richiesta HTTP GET per l'url, analizza il file
// risposta come HTML, estrae e restituisce i link. func
findLinks(url string) ([]string, error) {
resp, err := http.Get(url) if
err != nil {
restituire nil, err
}
se resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("getting %s: %s", url, resp.Status)
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("parsing %s as HTML: %v", url, err)
}
restituire visit(nil, doc), nil
}

Ci sono quattro dichiarazioni di ritorno in findLinks, ognuna delle quali restituisce una coppia di
valori. I primi tre ritorni fanno sì che la funzione passi gli errori sottostanti dai pacchetti http e html
al chiamante. Nel primo caso, l'errore viene restituito invariato; nel secondo e nel terzo, viene
aumentato con informazioni aggiuntive sul contesto da fmt.Errorf (§7.8). Se find- Links ha
successo, l'istruzione finale return restituisce la fetta di link, senza errori.
Dobbiamo assicurarci che resp.Body sia chiuso, in modo che le risorse di rete vengano rilasciate
correttamente anche in caso di errore. Il garbage collector di Go ricicla la memoria inutilizzata, ma non
bisogna pensare che rilasci le risorse del sistema operativo inutilizzate, come i file aperti e le connessioni
di rete. Devono essere chiuse esplicitamente.

www.it-ebooks.info
126 CAPITOLO 5. FUNZIONI

Il risultato della chiamata di una funzione multivariata è una tupla di valori. Il chiamante di una
funzione di questo tipo deve assegnare esplicitamente i valori alle variabili, se uno di essi deve essere
utilizzato:

link, err := findLinks(url)

Per ignorare uno dei valori, assegnarlo all'identificatore vuoto:

link, _ := findLinks(url) // errori ignorati

Il risultato di una chiamata multivariata può essere restituito da una funzione chiamante (multivariata),
come in questa funzione che si comporta come findLinks ma registra il suo argomento:

func findLinksLog(url string) ([]string, error) {


log.Printf("findLinks %s", url)
restituire findLinks(url)
}

Una chiamata multivariata può apparire come unico argomento quando si chiama una funzione con più
parametri. Sebbene sia raramente utilizzata nel codice di produzione, questa funzione è talvolta comoda
durante il debug, poiché consente di stampare tutti i risultati di una chiamata con una sola istruzione. Le
due istruzioni di stampa seguenti hanno lo stesso effetto.

log.Println(findLinks(url))

link, err := findLinks(url) log.Println(link, err)

Nomi ben scelti possono documentare il significato dei risultati di una funzione. I nomi sono
particolarmente utili quando una funzione restituisce più risultati dello stesso tipo, come nel caso di

func Size(rect image.Rectangle) (width, height int) func


Split(path string) (dir, file string)
func HourMinSec(t time.Time) (hour, minute, second int)

ma non sempre è necessario dare un nome a più risultati solo per la documentazione. Per esempio, la
convenzione vuole che un risultato finale bool indichi il successo; un risultato di errore spesso non ha
bisogno di spiegazioni.

In una funzione con risultati denominati, gli operandi di una dichiarazione di ritorno possono essere
omessi. Questo si chiama ritorno nudo.

// CountWordsAndImages esegue una richiesta HTTP GET per l'HTML


// documenta l'url e restituisce il numero di parole e immagini in esso contenute.
func CountWordsAndImages(url string) (words, images int, err error) {
resp, err := http.Get(url) if
err != nil {
ritorno
}

www.it-ebooks.info
SEZIONE 5.4. ERRORI 127

doc, err := html.Parse(resp.Body)


resp.Body.Close()
if err != nil {
err = fmt.Errorf("parsing HTML: %s", err) return
}
parole, immagini = countWordsAndImages(doc) return
}
func countWordsAndImages(n *html.Node) (words, images int) { /* ... */ }

Un ritorno nudo è un modo abbreviato per restituire ciascuna delle variabili di risultato nominate in
ordine, quindi nella funzione qui sopra, ogni dichiarazione di ritorno è equivalente a
restituire parole, immagini, errori

In funzioni come questa, con molte dichiarazioni di ritorno e diversi risultati, i ritorni nudi possono
ridurre la duplicazione del codice, ma raramente ne facilitano la comprensione. Per esempio, non è
evidente a prima vista che i due primi ritorni sono equivalenti a 0, 0, err (perché le variabili di
risultato words e images sono inizializzate al valore zero) e che il ritorno finale è equivalente a words,
images, nil. Per questo motivo, è meglio usare con parsimonia i ritorni nudi.
Esercizio 5.5: Implementare countWordsAndImages. (Vedere l'Esercizio 4.9 per la suddivisione delle parole).
Esercizio 5.6: Modificare la funzione corner in gopl.io/ch3/surface (§3.2) per utilizzare
risultati denominati e una dichiarazione di ritorno nuda.

5.4. Errori

Alcune funzioni riescono sempre a svolgere il loro compito. Ad esempio, strings.Contains e str-
conv.FormatBool hanno risultati ben definiti per tutti i possibili valori degli argomenti e non possono
fallire, a meno che non si verifichino scenari catastrofici e imprevedibili come l'esaurimento della
memoria, in cui il sintomo è lontano dalla causa e da cui c'è poca speranza di recupero.
Altre funzioni hanno sempre successo se le loro precondizioni sono soddisfatte. Ad esempio, la
funzione time.Date costruisce sempre un time.Time dai suoi componenti anno, mese e così via, a
meno che l'ultimo argomento (il fuso orario) non sia nullo, nel qual caso va in panico. Questo
panico è un segno sicuro di un bug nel codice chiamante e non dovrebbe mai verificarsi in un
programma ben scritto.
Per molte altre funzioni, anche in un programma ben scritto, il successo non è assicurato perché dipende
da fattori fuori dal controllo del programmatore. Qualsiasi funzione di I/O, ad esempio, deve affrontare
la possibilità di errore e solo un programmatore ingenuo crede che una semplice lettura o scrittura non
possa fallire. In effetti, è proprio quando le operazioni più affidabili falliscono inaspettatamente che
abbiamo bisogno di sapere perché.
Gli errori sono quindi una parte importante dell'API di un pacchetto o dell'interfaccia utente di
un'applicazione e il fallimento è solo uno dei tanti comportamenti previsti. Questo è l'approccio di Go
alla gestione degli errori.

www.it-ebooks.info
128 CAPITOLO 5. FUNZIONI

Una funzione per la quale il fallimento è un comportamento atteso restituisce un risultato aggiuntivo,
convenzionalmente l'ultimo. Se il fallimento ha una sola causa possibile, il risultato è un booleano, di
solito chiamato ok, come in questo esempio di ricerca nella cache che ha sempre successo a meno che
non ci sia nessuna voce per quella chiave:
valore, ok := cache.Lookup(chiave)
if !ok {
// ...cache[chiave] non esiste...
}

Più spesso, e soprattutto per l'I/O, il fallimento può avere una serie di cause per le quali il chiamante avrà
bisogno di una spiegazione. In questi casi, il tipo di risultato aggiuntivo è l'errore.
Il tipo built-in error è un tipo di interfaccia. Vedremo meglio cosa significa e quali sono le sue
implicazioni per la gestione degli errori nel Capitolo 7. Per ora è sufficiente sapere che un errore può
essere nil o non-nil, che nil implica un successo e non-nil un fallimento e c h e un errore non-nil ha una
stringa di messaggio di errore che si può ottenere chiamando il suo metodo Error o stampare chiamando
fmt.Println(err) o fmt.Printf("%v", err).

Di solito, quando una funzione restituisce un errore non nullo, gli altri risultati sono indefiniti e devono
essere ignorati. Tuttavia, alcune funzioni possono restituire risultati parziali in caso di errore. Ad
esempio, se si verifica un errore durante la lettura di un file, una chiamata a Read restituisce il numero di
byte che è riuscita a leggere e un valore di errore che descrive il problema. Per un comportamento
corretto, alcuni chiamanti potrebbero aver bisogno di elaborare i dati incompleti prima di gestire
l'errore, quindi è importante che tali funzioni documentino chiaramente i loro risultati.
L'approccio di Go lo distingue da quello di molti altri linguaggi, in cui i fallimenti vengono segnalati
utilizzando eccezioni e non valori ordinari. Sebbene Go abbia una sorta di meccanismo di eccezioni, come
vedremo nella Sezione 5.9, esso viene utilizzato solo per segnalare errori veramente inaspettati che
indicano un bug, non gli errori di routine che un programma robusto dovrebbe aspettarsi.
La ragione di questo progetto è che le eccezioni tendono a ingarbugliare la descrizione di un errore con il
flusso di controllo necessario per gestirlo, portando spesso a un risultato indesiderato: gli errori di
routine vengono segnalati all'utente finale sotto forma di una traccia di stack incomprensibile, piena di
informazioni sulla struttura del programma ma priva di un contesto intelligibile su ciò che è andato
storto.
Al contrario, i programmi Go utilizzano meccanismi di controllo ordinari come if e return per
rispondere agli errori. Questo stile richiede innegabilmente una maggiore attenzione alla logica di
gestione degli errori, ma è proprio questo il punto.

5.4.1. Strategie di gestione degli errori

Quando una chiamata di funzione restituisce un errore, è responsabilità del chiamante controllarlo e
prendere le misure appropriate. A seconda della situazione, ci possono essere diverse possibilità.
Vediamo cinque di queste.

www.it-ebooks.info
SEZIONE 5.4. ERRORI 129

Il primo, e più comune, è quello di propagare l'errore, in modo che un errore in una subroutine diventi
un errore della routine chiamante. Ne abbiamo visto un esempio nella funzione findLinks della Sezione
5.3. Se la chiamata a http.Get fallisce, findLinks restituisce l'errore HTTP al chiamante, senza ulteriori
indugi:
resp, err := http.Get(url) if
err != nil {
restituire nil, err
}

Al contrario, se la chiamata a html.Parse fallisce, findLinks non restituisce direttamente l'errore del
parser HTML perché manca di due informazioni cruciali: che l'errore si è verificato nel parser e l'URL
del documento che veniva analizzato. In questo caso, findLinks crea un nuovo messaggio di errore che
include entrambe le informazioni e l'errore di parsing sottostante:
doc, err := html.Parse(resp.Body) resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("parsing %s as HTML: %v", url, err)
}

La funzione fmt.Errorf formatta un messaggio di errore usando fmt.Sprintf e restituisce un nuovo valore
di errore. La usiamo per costruire errori descrittivi, aggiungendo successivamente ulteriori informazioni
di contesto al messaggio di errore originale. Quando l'errore viene gestito dalla funzione principale del
programma, dovrebbe fornire una chiara catena causale dal problema alla radice al fallimento generale,
come in un'indagine NASA sugli incidenti:
genesis: schiantato: nessun paracadute: Interruttore G fallito: cattivo orientamento del relè

Poiché i messaggi di errore sono spesso concatenati, le stringhe dei messaggi non dovrebbero essere
maiuscole e le linee nuove dovrebbero essere evitate. Gli errori risultanti possono essere lunghi, ma
saranno autocontenuti quando verranno trovati da strumenti come grep.

Quando si progettano i messaggi di errore, bisogna essere deliberati, in modo che ognuno di essi sia una
descrizione significativa del problema con dettagli sufficienti e pertinenti, e coerenti, in modo che gli
errori restituiti dalla stessa funzione o da un gruppo di funzioni dello stesso pacchetto siano simili nella
forma e possano essere trattati nello stesso modo.

Per esempio, il pacchetto os garantisce che ogni errore restituito da un'operazione su un file, come
os.Open o i metodi Read, Write o Close di un file aperto, descriva non solo la natura del fallimento
(permesso negato, nessuna directory simile e così via) ma anche il nome del file, in modo che il
chiamante non debba includere queste informazioni nel messaggio di errore che costruisce.

In generale, la chiamata f(x) è responsabile della segnalazione dell'operazione tentata f e del valore
argomentativo x in relazione al contesto dell'errore. Il chiamante è responsabile dell'aggiunta di ulteriori
informazioni di cui dispone ma che la chiamata f(x) non possiede, come l'URL nella chiamata
a html.Parse di cui sopra.

www.it-ebooks.info
130 CAPITOLO 5. FUNZIONI

Passiamo alla seconda strategia per la gestione degli errori. Per gli errori che rappresentano problemi
transitori o imprevedibili, può avere senso riprovare l'operazione fallita, eventualmente con un ritardo
tra un tentativo e l'altro e forse con un limite al numero di tentativi o al tempo trascorso prima di
rinunciare del tutto.
gopl.io/ch5/attesa
// WaitForServer tenta di contattare il server di un URL.
// Tenta per un minuto utilizzando un back-off esponenziale.
// Segnala un errore se tutti i tentativi
falliscono. func WaitForServer(url string) error
{
const timeout = 1 * time.Minute deadline :=
time.Now().Add(timeout)
for tries := 0; time.Now().Before(deadline); tries++ {
_, err := http.Head(url) if
err == nil {
return nil // successo
}
log.Printf("Il server non risponde (%s); riprova...", err) time.Sleep(time.Second <<
uint(tries)) // back-off esponenziale
}
return fmt.Errorf("il server %s non ha risposto dopo %s", url, timeout)
}

In terzo luogo, se l'avanzamento è impossibile, il chiamante può stampare l'errore e interrompere il


programma con grazia, ma questo tipo di azione dovrebbe essere generalmente riservata al pacchetto
principale di un programma. Le funzioni di libreria dovrebbero di solito propagare gli errori al
chiamante, a meno che l'errore non sia il segno di un'incoerenza interna, cioè di un bug.
// (Nella funzione main.)
if err := WaitForServer(url); err := nil { fmt.Fprintf(os.Stderr,
"Site is down: %v\n", err) os.Exit(1)
}

Un modo più comodo per ottenere lo stesso effetto è chiamare log.Fatalf. Come per tutti i log
per impostazione predefinita, aggiunge al messaggio di errore il prefisso della data e dell'ora.
if err := WaitForServer(url); err := nil {
log.Fatalf("Site is down: %v\n", err)
}

Il formato predefinito è utile in un server di lunga durata, ma meno per uno strumento interattivo:
2006/01/02 15:04:05 Il sito è inattivo: nessun dominio di questo tipo: bad.gopl.io

Per un output più accattivante, si può impostare il prefisso usato dal pacchetto log al nome del
comando e sopprimere la visualizzazione di data e ora:
log.SetPrefix("wait: ")
log.SetFlags(0)

www.it-ebooks.info
SEZIONE 5.4. ERRORI 131

In quarto luogo, in alcuni casi è sufficiente registrare l'errore e poi continuare, magari con
funzionalità ridotte. Anche in questo caso si può scegliere tra l'uso del pacchetto log, che aggiunge il
solito prefisso:
if err := Ping(); err : = nil {
log.Printf("ping fallito: %v; rete disattivata", err)
}

e stampare direttamente sul flusso di errore standard:


if err := Ping(); err : = nil {
fmt.Fprintf(os.Stderr, "ping fallito: %v; rete disabilitata", err)
}

(Tutte le funzioni di log aggiungono un newline se non è già presente).


E infine, in rari casi, possiamo ignorare completamente un errore:
dir, err := ioutil.TempDir("", "scratch") if
err := nil {
return fmt.Errorf("failed to create temp dir: %v", err)
}

// ...usa la directory temporanea...

os.RemoveAll(dir) // ignora gli errori; $TMPDIR viene pulito periodicamente

La chiamata a os.RemoveAll può fallire, ma il programma la ignora perché il sistema operativo pulisce
periodicamente la directory temporanea. In questo caso, scartare l'errore è stato intenzionale, ma la
logica del programma sarebbe stata la stessa se avessimo dimenticato di occuparcene. Prendete
l'abitudine di considerare gli errori dopo ogni chiamata di funzione e, quando ne ignorate
deliberatamente uno, documentate chiaramente la vostra intenzione.
La gestione degli errori in Go ha un ritmo particolare. Dopo aver controllato un errore, il fallimento
viene solitamente trattato prima del successo. Se il fallimento causa il ritorno della funzione, la logica per
il successo non è indentata all'interno di un blocco else, ma segue a livello esterno. Le funzioni tendono
a presentare una struttura comune, con una serie di controlli iniziali per rifiutare gli errori, seguiti dalla
sostanza della funzione alla fine, minimamente rientrata.

5.4.2. Fine del file (EOF)

Di solito, la varietà di errori che una funzione può restituire è interessante per l'utente finale, ma non per
la logica del programma che interviene. A volte, tuttavia, un programma deve intraprendere azioni
diverse a seconda del tipo di errore che si è verificato. Si consideri il tentativo di leggere n byte di dati da
un file. Se n è scelto come lunghezza del file, qualsiasi errore rappresenta un fallimento. D'altra parte, se
il chiamante tenta ripetutamente di leggere pezzi di dimensioni fisse fino a esaurire il file, deve
rispondere in modo diverso a una condizione di fine file rispetto a tutti gli altri errori. Per questo
motivo, il pacchetto io garantisce che qualsiasi errore di lettura causato da una condizione di fine file sia
sempre segnalato da un errore distinto, io.EOF, che è definito come segue:

www.it-ebooks.info
132 CAPITOLO 5. FUNZIONI

pacchetto io
importare
"errori"
// EOF è l'errore restituito da Read quando non sono d i s p o n i b i l i altri input. var
EOF = errors.New("EOF")

Il chiamante può rilevare questa condizione con un semplice confronto, come nel ciclo seguente, che
legge le rune dallo standard input. (Il programma charcount della Sezione 4.3 fornisce un esempio più
completo).
in := bufio.NewReader(os.Stdin) per {
r, _, err := in.ReadRune() if
err == io.EOF {
break // lettura finita
}
if err != nil {
return fmt.Errorf("lettura fallita: %v", err)
}
// ...utilizzare r...
}

Poiché in una condizione di fine file non ci sono informazioni da riportare oltre al fatto, io.EOF ha un
messaggio di errore fisso, "EOF". Per altri errori, potrebbe essere necessario segnalare sia la qualità che la
quantità dell'errore, quindi un valore di errore fisso non è sufficiente. Nella Sezione 7.11
presenteremo un modo più sistematico per distinguere alcuni valori di errore da altri.

5.5. Funzione Valori

Le funzioni sono valori di prima classe in Go: come gli altri valori, i valori delle funzioni hanno dei tipi e
possono essere assegnati a variabili o passati a funzioni o restituiti da esse. Un valore di funzione può
essere chiamato come qualsiasi altra funzione. Ad esempio:
func quadrato(n int) int { restituisce n * n }
func negative(n int) int { restituisce -n }
func product(m, n int) int { restituisce m * n }

f := quadrato
fmt.Println(f(3)) // "9"

f = negativo fmt.Println(f(3))
// "-3"
fmt.Printf("%T\n", f) // "func(int) int"

f = prodotto // errore di compilazione: non è possibile assegnare f(int, int) int a f(int) int

Il valore zero di un tipo di funzione è nil. La chiamata di un valore di funzione nullo provoca un panico:
var f func(int) int
f(3) // panico: chiamata di una funzione nil

www.it-ebooks.info
SEZIONE 5.5. VALORI DELLE FUNZIONI 133

I valori delle funzioni possono essere confrontati con nil:


var f func(int) int if
f != nil {
f(3)
}

ma non sono comparabili, quindi non possono essere confrontati tra loro o usati come chiavi in una
mappa.
I valori delle funzioni ci permettono di parametrizzare le nostre funzioni non solo sui dati, ma
anche sul comportamento. Le librerie standard contengono molti esempi. Ad esempio, strings.Map
applica una funzione a ciascun carattere di una stringa, unendo i risultati per creare un'altra stringa.
func add1(r rune) rune { return r + 1 }

fmt.Println(strings.Map(add1, "HAL-9000")) // "IBM.:111"


fmt.Println(strings.Map(add1, "VMS")) // "WNT"

fmt.Println(strings.Map(add1, "Admix")) // "Benjy"

La funzione findLinks della Sezione 5.2 utilizza una funzione ausiliaria, visit, per visitare tutti i nodi di
un documento HTML e applicare un'azione a ciascuno di essi. Utilizzando il valore di una funzione,
possiamo separare la logica per l'attraversamento dell'albero dalla logica per l'azione da applicare a
ciascun nodo, consentendo di riutilizzare l'attraversamento con azioni diverse.
gopl.io/ch5/outline2
// forEachNode richiama le funzioni pre(x) e post(x) per ogni nodo
// x nell'albero con radice in n. Entrambe le funzioni sono opzionali.
// pre viene chiamato prima che i bambini vengano visitati (preorder) e
// post viene chiamato dopo (postorder).
func forEachNode(n *html.Node, pre, post func(n *html.Node)) { if pre !=
nil {
pre(n)
}

per c := n.FirstChild; c := nil; c = c.NextSibling {


forEachNode(c, pre, post)
}

if post != nil {
post(n)
}
}

La funzione forEachNode accetta due argomenti di funzione, uno da chiamare prima che i figli di un nodo
vengano visitati e uno da chiamare dopo. Questa disposizione offre al chiamante una grande flessibilità.
Ad esempio, le funzioni startElement e endElement stampano i tag iniziale e finale di un elemento HTML
come <b>...</b>:
var profondità int

www.it-ebooks.info
134 CAPITOLO 5. FUNZIONI

func startElement(n *html.Node) { if


n.Type == html.ElementNode {
fmt.Printf("%*s<%s>\n", depth*2, "", n.Data) depth++
}
}
func endElement(n *html.Node) {
se n.Type == html.ElementNode {
depth--
fmt.Printf("%*s</%s>\n", depth*2, "", n.Data)
}
}

Le funzioni indentano anche l'output utilizzando un altro trucco di fmt.Printf. L'avverbio * in %*s
stampa una stringa imbottita con un numero variabile di spazi. La larghezza e la stringa sono
fornite dagli argomenti depth*2 e "".
Se si chiama forEachNode su un documento HTML, come questo:
forEachNode(doc, startElement, endElement)

otteniamo una variante più elaborata dell'output del nostro precedente programma di contorno:
$ go build gopl.io/ch5/outline2
$ ./outline2 http://gopl.io
<html>
<head>
<meta>
</meta>
<titolo>
</title>
<style>
</style>
</head>
<body>
<tabella>
<tbody>
<tr>
<td>
<a>
<img>
</img>.
...

Esercizio 5.7: Sviluppare startElement e endElement in un pretty-printer HTML generale. Stampate i


nodi di commento, i nodi di testo e gli attributi di ogni elemento (<a href='...'>). Usare forme brevi
come <img/> invece di <img></img> quando un elemento non ha figli. Scrivere un test per assicurarsi che
l'output possa essere analizzato correttamente. (Vedere il Capitolo 11).
Esercizio 5.8: Modificare forEachNode in modo che le funzioni pre e post restituiscano un risultato
booleano che indica se continuare l'attraversamento. Utilizzatela per scrivere una funzione
ElementByID con l'attributo

www.it-ebooks.info
SEZIONE 5.6. FUNZIONI ANONIME 135

che trova il primo elemento HTML con l'attributo id specificato. La funzione deve interrompere
l'attraversamento non appena viene trovata una corrispondenza.
func ElementByID(doc *html.Node, id string) *html.Node

Esercizio 5.9: Scrivere una funzione expand(s stringa, f func(stringa) stringa) che
sostituisca ogni sottostringa ''$pippo'' all'interno di s con il testo restituito da f("pippo").

5.6. Funzioni anonime

Le funzioni con nome possono essere dichiarate solo a livello di pacchetto, ma è possibile utilizzare un
letterale di funzione per indicare un valore di funzione all'interno di qualsiasi espressione. Un letterale di
funzione è scritto come una dichiarazione di funzione, ma senza il nome che segue la parola chiave func.
È un'espressione e il suo valore è chiamato funzione anonima.
I letterali di funzione ci permettono di definire una funzione al momento del suo utilizzo. Ad esempio, la
precedente chiamata a
strings.Map può essere riscritto come
strings.Map(func(r rune) rune { return r + 1 }, "HAL-9000")

Inoltre, le funzioni definite in questo modo hanno accesso all'intero ambiente lessicale, quindi la
funzione interna può fare riferimento alle variabili della funzione racchiusa, come mostra questo
esempio:
gopl.io/ch5/quadri
// quadra restituisce una funzione che restituisce
// il numero quadrato successivo ogni volta che viene
chiamato. func squares() func() int {
var x int
restituire func() int {
x++
restituire x * x
}
}
func main() {
f := quadrati()
fmt.Println(f()) // "1"
fmt.Println(f()) // "4"
fmt.Println(f()) // "9"
fmt.Println(f()) // "16"
}

La funzione squares restituisce un'altra funzione, di tipo func() int. Una chiamata a squares crea una
variabile locale x e restituisce una funzione anonima che, ogni volta che viene chiamata, incrementa
x e restituisce il suo quadrato. Una seconda chiamata a squares creerebbe una seconda variabile x e
restituirebbe una nuova funzione anonima che incrementa tale variabile.
L'esempio dei quadrati dimostra che i valori delle funzioni non sono solo codice, ma possono avere uno
stato. La funzione interna anonima può accedere e aggiornare le variabili locali della funzione interna

www.it-ebooks.info
136 CAPITOLO 5. FUNZIONI

quadrati di funzioni. Questi riferimenti nascosti alle variabili sono il motivo per cui le funzioni sono
classificate come tipi di riferimento e i valori delle funzioni non sono comparabili. Valori di funzione
come questi sono implementati con una tecnica chiamata closures e i programmatori Go usano spesso
questo termine per i valori di funzione.
Anche in questo caso vediamo un esempio in cui la durata di una variabile non è determinata dal
suo ambito: la variabile x esiste dopo il ritorno di squares all'interno di main, anche se x è nascosta
dentro f.
Come esempio un po' accademico di funzioni anonime, si consideri il problema di comporre una
sequenza di corsi di informatica che soddisfi i requisiti di prerequisito di ciascuno di essi. I prerequisiti
sono indicati nella tabella dei prerequisiti che segue, che è una mappatura da ogni corso all'elenco dei corsi
che devono essere completati prima di esso.
gopl.io/ch5/toposort
// prereqs mappa i corsi di informatica ai loro prerequisiti. var prereqs =
map[string][]string{
"algoritmi": {"strutture dati"},
"calcolo": {"algebra lineare"},

"compilatori": {
"strutture dati", "linguaggi
formali", "organizzazione
informatica",
},

"strutture dati": {"matematica discreta"},


"database": {"strutture dati"},
"matematica discreta": {"introduzione alla
programmazione"}, "linguaggi formali": {"matematica
discreta"}, "reti": {"sistemi operativi"},
"sistemi operativi": {"strutture dati", "organizzazione informatica"},
"linguaggi di programmazione": {"s t r u t t u r e dati", "organizzazione informatica"},
}

Questo tipo di problema è noto come ordinamento topologico. Concettualmente, le informazioni sui
prerequisiti formano un grafo diretto con un nodo per ogni corso e spigoli da ogni corso ai corsi da cui
dipende. Il grafo è aciclico: non esiste un percorso che da un corso porti a se stesso. Possiamo calcolare
una sequenza valida utilizzando la ricerca depth-first attraverso il grafo con il codice seguente:
func main() {
per i, corso := intervallo topoSort(prereqs) {
fmt.Printf("%d:\t%s\n", i+1, corso)
}
}

func topoSort(m map[string][]string) []string { var


order []string
seen := make(map[string]bool)
var visitAll func(items []string)

www.it-ebooks.info
SEZIONE 5.6. FUNZIONI ANONIME 137

visitAll = func(items []string) { for _,


item := range items {
if !seen[item] {
seen[item] = true
visitAll(m[item])
ordine = append(ordine, elemento)
}
}
}
var keys []string for
key := range m {
chiavi = append(chiavi, chiave)
}
sort.Strings(chiavi)
visitAll(chiavi) return
order
}

Quando una funzione anonima richiede una ricorsione, come in questo esempio, dobbiamo prima
dichiarare una variabile e poi assegnare la funzione anonima a quella variabile. Se questi due passaggi
fossero stati combinati nella dichiarazione, il letterale della funzione non sarebbe rientrato nell'ambito
della variabile visitAll e quindi non avrebbe avuto modo di richiamare se stessa in modo ricorsivo:
visitAll := func(items []string) {
// ...
visitAll(m[item]) // errore di compilazione: undefined: visitAll
// ...
}

L'output del programma toposort è mostrato di seguito. È deterministico, una proprietà spesso
desiderabile che non è sempre gratuita. In questo caso, i valori della mappa prereqs sono fette, non
altre mappe, quindi il loro ordine di iterazione è deterministico e abbiamo ordinato le chiavi di prereqs
prima di effettuare le chiamate iniziali a visitAll.
1: introduzione alla programmazione
2: matematica discreta
3: strutture dati
4: algoritmi
5: algebra lineare
6: calcolo
7: linguaggi formali
8: organizzazione del computer
9: compilatori
10: banche dati
11: sistemi operativi
12: reti
13: linguaggi di programmazione

Torniamo al nostro esempio di findLinks. Abbiamo spostato la funzione di estrazione dei collegamenti
link.Extract in un pacchetto a sé stante, dato che lo useremo di nuovo nel Capitolo 8. Abbiamo
sostituito il file

www.it-ebooks.info
138 CAPITOLO 5. FUNZIONI

con una funzione anonima che aggiunge direttamente alla slice dei link e utilizza forEachNode per
gestire l'attraversamento. Poiché Extract ha bisogno solo della funzione pre, passa nil per
l'argomento post.
gopl.io/ch5/links
// Il pacchetto link fornisce una funzione di estrazione dei link.
pacchetto link

importare (
"fmt"
"net/http"

"golang.org/x/net/html"
)

// L'estratto esegue una richiesta HTTP GET all'URL specificato, analizza


// la risposta come HTML e restituisce i link nel documento HTML. func Extract(url
string) ([]string, error) {
resp, err := http.Get(url) if
err != nil {
restituire nil, err
}
se resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("getting %s: %s", url, resp.Status)
}

doc, err := html.Parse(resp.Body)


resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("parsing %s as HTML: %v", url, err)
}

var link []stringa


visitNode := func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "a" { for _, a
:= range n.Attr {
se a.Key = "href" {
continuare
}
link, err := resp.Request.URL.Parse(a.Val) if err
!= nil {
continuare // ignorare gli URL non corretti
}
link = append(link, link.String())
}
}
}
forEachNode(doc, visitNode, nil) return
link, nil
}

www.it-ebooks.info
SEZIONE 5.6. FUNZIONI ANONIME 139

Invece di aggiungere il valore grezzo dell'attributo href alla slice dei link, questa versione lo analizza come
URL relativo all'URL di base del documento, resp.Request.URL. Il link risultante è in forma
assoluta, adatto per essere utilizzato in una chiamata a http.Get.

Il crawling del Web è, in fondo, un problema di attraversamento di grafi. L'esempio di topoSort


mostrava una traversata depth-first; per il nostro web crawler useremo una traversata breadth-first,
almeno inizialmente. Nel Capitolo 8 esploreremo l'attraversamento concorrente.

La funzione sottostante racchiude l'essenza di un attraversamento breadth-first. Il chiamante


fornisce un elenco iniziale di elementi da visitare e un valore di funzione f da richiamare per ogni
elemento. Ogni elemento è identificato da una stringa. La funzione f restituisce un elenco di nuovi
elementi da aggiungere all'elenco di lavoro. La funzione breadthFirst ritorna quando tutti gli
elementi sono stati visitati. Mantiene un insieme di stringhe per garantire che nessun elemento
venga visitato due volte.

gopl.io/ch5/findlinks3
// breadthFirst chiama f per ogni elemento dell'elenco di lavoro.
// Tutti gli elementi restituiti da f vengono aggiunti alla lista di lavoro.
// f viene chiamata al massimo una volta per ogni elemento.
func breadthFirst(f func(item string) []string, worklist []string) { seen :=
make(map[string]bool)
for len(worklist) > 0 { items :=
worklist worklist = nil
for _, item := range items { if
!seen[item] {
seen[item] = true
lista di lavoro = append(lista di lavoro, f(item)...)
}
}
}
}

Come abbiamo spiegato di sfuggita nel Capitolo 3, l'argomento ''f(item)...'' fa sì che tutti gli elementi
dell'elenco restituito da f vengano aggiunti alla lista di lavoro.

Nel nostro crawler, gli elementi sono URL. La funzione di crawl che forniremo a breadthFirst stampa
l'URL, estrae i suoi collegamenti e li restituisce in modo che vengano visitati.

func crawl(url string) []string { fmt.Println(url)


list, err := links.Extract(url) if
err := nil {
log.Print(err)
}
restituire l'elenco
}

Per avviare il crawler, useremo gli argomenti della riga di comando come URL iniziali.

www.it-ebooks.info
140 CAPITOLO 5. FUNZIONI

func main() {
// Eseguire il crawling del web per primo,
// partendo dagli argomenti della riga di comando.
breadthFirst(crawl, os.Args[1:])
}

Effettuiamo il crawling del web partendo da https://golang.org. Ecco alcuni dei link risultanti:
$ go build gopl.io/ch5/findlinks3
$ ./findlinks3 https://golang.org
https://golang.org/
https://golang.org/doc/
https://golang.org/pkg/
https://golang.org/project/
https://code.google.com/p/go-tour/
https://golang.org/doc/code.html
https://www.youtube.com/watch?v=XCsL89YtqCs
http://research.swtch.com/gotour
https://vimeo.com/53221560
...

Il processo termina quando tutte le pagine web raggiungibili sono state scansionate o la memoria del
computer è esaurita.
Esercizio 5.10: Riscrivere topoSort per utilizzare le mappe al posto delle fette ed eliminare l'ordinamento
iniziale. Verificare che i risultati, anche se non deterministici, siano ordinamenti topologici validi.
Esercizio 5.11: L'istruttore del corso di algebra lineare decide che il calcolo è ora un prerequisito.
Estendere la funzione topoSort per segnalare i cicli.
Esercizio 5.12: Le funzioni startElement e endElement in gopl.io/ch5/outline2 (§5.5) condividono
una variabile globale, depth. Trasformatele in funzioni anonime che condividono una variabile locale
della funzione outline.
Esercizio 5.13: Modificare il crawl per creare copie locali delle pagine che trova, creando le directory
necessarie. Non fare copie di pagine che provengono da un dominio diverso. Ad esempio, se la pagina
originale proviene da golang.org, salvare tutti i file da lì, ma escludere quelli da vimeo.com.
Esercizio 5.14: Usare la funzione breadthFirst per esplorare una struttura diversa. Ad esempio, si
possono usare le dipendenze dei corsi dell'esempio topoSort (un grafo diretto), la gerarchia del file system
del computer (un albero) o un elenco di percorsi dell'autobus o della metropolitana scaricati dal sito web
dell'amministrazione comunale (un grafo non diretto).

5.6.1. Avvertenza: catturare le variabili di iterazione

In questa sezione esamineremo una trappola delle regole di ambito lessicale di Go che può causare
risultati sorprendenti. Vi invitiamo a comprendere il problema prima di procedere, perché la trappola
può intrappolare anche programmatori esperti.

www.it-ebooks.info
SEZIONE 5.6. FUNZIONI ANONIME 141

Consideriamo un programma che deve creare un insieme di directory e successivamente rimuoverle.


Possiamo usare una fetta di valori di funzioni per contenere le operazioni di pulizia. (Per brevità, in
questo esempio abbiamo omesso la gestione degli errori).
var rmdirs []func()
per _, d := intervallo tempDirs() {
dir := d // NOTA: necessario! os.MkdirAll(dir,
0755) // crea anche le directory dei genitori rmdirs =
append(rmdirs, func() {
os.RemoveAll(dir)
})
}

// ... fare un po' di lavoro...

for _, rmdir := range rmdirs { rmdir()


// pulizia
}

Vi chiederete perché abbiamo assegnato la variabile del ciclo d a una nuova variabile locale dir
all'interno del corpo del ciclo, invece di nominare semplicemente la variabile del ciclo dir come in
questa variante sottilmente scorretta:
var rmdirs []func()
per _, dir := range tempDirs() { os.MkdirAll(dir,
0755)
rmdirs = append(rmdirs, func() { os.RemoveAll(dir) //
NOTA: non è corretto!
})
}

Il motivo è una conseguenza delle regole di scope per le variabili dei cicli. Nel programma
immediatamente precedente, il ciclo for introduce un nuovo blocco lessicale in cui è dichiarata la
variabile dir. Tutti i valori delle funzioni create da questo ciclo ''catturano'' e condividono la stessa
variabile, una posizione di memoria indirizzabile, non il suo valore in quel particolare momento. Il
valore di dir viene aggiornato in iterazioni successive, quindi quando vengono chiamate le funzioni di
pulizia, la variabile dir è stata aggiornata più volte dal ciclo for completato. Pertanto dir conserva il
valore dell'ultima iterazione e di conseguenza tutte le chiamate a os.RemoveAll tenteranno di rimuovere
la stessa directory.
Spesso, la variabile interna introdotta per aggirare questo problema (nel nostro esempio, DIR) ha lo
stesso nome della variabile esterna di cui è una copia, il che porta a dichiarazioni di variabili strane ma
cruciali come questa:
per _, dir := range tempDirs() {
dir := dir // dichiara il dir interno, inizializzato al dir esterno
// ...
}

Il rischio non riguarda solo i cicli for basati sull'intervallo. Il ciclo nell'esempio seguente soffre dello
stesso problema a causa della cattura involontaria della variabile indice i.

www.it-ebooks.info
142 CAPITOLO 5. FUNZIONI

var rmdirs []func() dirs


:= tempDirs()
for i := 0; i < len(dirs); i++ {
os.MkdirAll(dirs[i], 0755) // OK rmdirs
= append(rmdirs, func() {
os.RemoveAll(dirs[i]) // NOTA: non è corretto!
})
}

Il problema della cattura della variabile di iterazione si presenta più spesso quando si usa lo stato go
(Capitolo 8) o con defer (che vedremo tra poco), poiché entrambi possono ritardare l'esecuzione di
un valore di funzione fino al termine del ciclo. Ma il problema non è inerente a go o defer.

5.7. Variadic Funzioni

Una funzione variadica è una funzione che può essere chiamata con un numero variabile di argomenti.
Gli esempi più familiari sono fmt.Printf e le sue varianti. Printf richiede un argomento fisso all'inizio,
poi accetta un numero qualsiasi di argomenti successivi.

Per dichiarare una funzione variadica, il tipo del parametro finale è preceduto da un'ellissi, ''...'', che
indica che la funzione può essere chiamata con un numero qualsiasi di argomenti di questo tipo.
gopl.io/ch5/sum
func sum(vals ...int) int { total :=
0
per _, val := intervallo vals {
totale += val
}
ritorno totale
}

La funzione sum di cui sopra restituisce la somma di zero o più argomenti int. Nel corpo della
funzione, il tipo di vals è una slice []int. Quando viene richiamata la funzione sum, è possibile
fornire un numero qualsiasi di valori per il parametro vals.
fmt.Println(sum()) // " 0"
fmt.Println(sum(3)) // " 3"
fmt.Println(somma(1, 2, 3, 4)) // " 10"

Implicitamente, il chiamante alloca un array, vi copia gli argomenti e passa una fetta dell'intero array alla
funzione. L'ultima chiamata sopra si comporta quindi come la chiamata sotto, che mostra come invocare
una funzione variadica quando gli argomenti sono già in una fetta: inserire un'ellissi dopo l'ultimo
argomento.
valori := []int{1, 2, 3, 4}
fmt.Println(somma(valori...)) // "10"

www.it-ebooks.info
SEZIONE 5.8. CHIAMATE DI FUNZIONE DIFFERITE 143

Sebbene il parametro ...int si comporti come una slice all'interno del corpo della funzione, il tipo di
una funzione variadica è distinto dal tipo di una funzione con un normale parametro slice.
func f(...int) {}
func g([]int) {}

fmt.Printf("%T\n", f) // "func(...int)"
fmt.Printf("%T\n", g) // "func([]int)"

Le funzioni variabili sono spesso utilizzate per la formattazione delle stringhe. La funzione errorf
qui sotto contiene un messaggio di errore formattato con un numero di riga all'inizio. Il suffisso f è una
convenzione di denominazione ampiamente seguita per le funzioni variadiche che accettano una
stringa di formato in stile Printf.
func errorf(linenum int, format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, "Line %d: ", linenum) fmt.Fprintf(os.Stderr,
format, args...) fmt.Fprintln(os.Stderr)
}

linenum, nome := 12, "conteggio"


errorf(linenum, "undefined: %s", name) // "Riga 12: undefined: count"

Il tipo interface{} significa che questa funzione può accettare qualsiasi valore per i suoi argomenti finali,
come verrà spiegato nel Capitolo 7.
Esercizio 5.15: Scrivere le funzioni variadiche max e min, analoghe alla somma. Cosa devono fare queste
funzioni quando vengono chiamate senza argomenti? Scrivete le varianti che richiedono almeno un
argomento.
Esercizio 5.16: Scrivere una versione variadica di strings.Join.
Esercizio 5.17: Scrivere una funzione variadica ElementsByTagName che, dato un albero di nodi HTML
e zero o più nomi, restituisca tutti gli elementi che corrispondono a uno di questi nomi. Ecco due
esempi di chiamata:
func ElementsByTagName(doc *html.Node, name ...string) []*html.Node images :=

ElementsByTagName(doc, "img")
titoli := ElementsByTagName(doc, "h1", "h2", "h3", "h4")

5.8. Funzione differita Chiamate

I nostri esempi di findLinks hanno utilizzato l'output di http.Get come input di html.Parse. Questo
funziona bene se il contenuto dell'URL richiesto è effettivamente HTML, ma molte pagine contengono
immagini, testo semplice e altri formati di file. L'inserimento di tali file in un parser HTML potrebbe
avere effetti indesiderati.
Il programma seguente recupera un documento HTML e ne stampa il titolo. La funzione title
controlla l'intestazione Content-Type della risposta del server e restituisce un errore se il documento
non è HTML.

www.it-ebooks.info
144 CAPITOLO 5. FUNZIONI

gopl.io/ch5/titolo1
func title(url string) error {
resp, err := http.Get(url) if
err != nil {
restituire err
}
// Verificare che il tipo di contenuto sia HTML (ad esempio, "text/html;
charset=utf-8"). ct := resp.Header.Get("Content-Type")
if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") { resp.Body.Close()
return fmt.Errorf("%s ha tipo %s, non text/html", url, ct)
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return fmt.Errorf("parsing %s as HTML: %v", url, err)
}
visitNode := func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "title" &&
n.FirstChild != nil { fmt.Println(n.FirstChild.Data)
}
}
forEachNode(doc, visitNode, nil) return
nil
}

Ecco una sessione tipica, leggermente modificata per adattarla:


$ go build gopl.io/ch5/title1
$ ./title1 http://gopl.io Il
linguaggio di programmazione Go
$ ./title1 https://golang.org/doc/effective_go.html Go efficace - Il
linguaggio di programmazione Go
$ ./title1 https://golang.org/doc/gopher/frontpage.png titolo:
https://golang.org/doc/gopher/frontpage.png
ha il tipo immagine/png, non testo/html

Osservare la duplicazione della chiamata resp.Body.Close(), che assicura che title chiuda la
connessione alla rete in tutti i percorsi di esecuzione, compresi i fallimenti. Man mano che le funzioni
diventano più complesse e devono gestire un maggior numero di errori, questa duplicazione della logica
di pulizia può diventare un problema di manutenzione. Vediamo come il nuovo meccanismo di rinvio
di Go semplifica le cose.
Dal punto di vista sintattico, un'istruzione defer è una normale chiamata di funzione o di metodo
preceduta dalla parola chiave defer. Le espressioni della funzione e degli argomenti vengono
valutate quando l'istruzione viene eseguita, ma la chiamata vera e propria viene rinviata fino a
quando la funzione che contiene l'istruzione defer non è terminata, sia in modo normale, con
l'esecuzione di un'istruzione di ritorno o con la caduta della fine, sia in modo anomalo, con il
panico. È possibile rinviare un numero qualsiasi di chiamate, che vengono eseguite nell'istruzione

www.it-ebooks.info
SEZIONE 5.8. CHIAMATE DI FUNZIONE DIFFERITE 145

inverso rispetto all'ordine in cui sono stati rinviati.

L'istruzione defer viene spesso utilizzata con operazioni accoppiate come open e close, connect e dis-
connect, o lock e unlock, per garantire che le risorse vengano rilasciate in tutti i casi, indipendentemente
dalla complessità del flusso di controllo. Il posto giusto per un'istruzione defer che rilascia una risorsa è
immediatamente dopo che la risorsa è stata acquisita con successo. Nella funzione del titolo qui sotto,
una singola chiamata differita sostituisce entrambe le precedenti chiamate a resp.Body.Close():

gopl.io/ch5/titolo2
func title(url string) error {
resp, err := http.Get(url) if
err != nil {
restituire err
}
rinviare resp.Body.Close()

ct := resp.Header.Get("Content-Type")
if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") { return
fmt.Errorf("%s ha tipo %s, non text/html", url, ct)
}

doc, err := html.Parse(resp.Body) if err


:= nil {
return fmt.Errorf("parsing %s as HTML: %v", url, err)
}

// ... stampare l'elemento titolo del documento...

restituire nil
}

Lo stesso schema può essere utilizzato per altre risorse oltre alle connessioni di rete, ad esempio per
chiudere un file aperto:
io/ioutil
pacchetto ioutil

func ReadFile(filename string) ([]byte, error) { f, err :=


os.Open(filename)
if err != nil { return
nil, err
}
defer f.Close() return
ReadAll(f)
}

o per sbloccare un mutex (§9.2):


var mu sync.Mutex
var m = make(map[string]int)

www.it-ebooks.info
146 CAPITOLO 5. FUNZIONI

func lookup(key string) int {


mu.Lock()
deferisci mu.Unlock()
return m[key]
}

L'istruzione defer può essere usata anche per accoppiare le azioni ''on entry'' e ''on exit'' quando si esegue
il debug di una funzione complessa. La funzione bigSlowOperation qui sotto chiama
immediatamente trace, che esegue l'azione ''on entry'' e poi restituisce un valore di funzione che,
una volta richiamato, esegue l'azione ''on exit'' corrispondente. Rinviando la chiamata alla funzione
restituita in questo modo, possiamo strumentare il punto di ingresso e tutti i punti di uscita di una
funzione in un'unica istruzione e persino passare valori, come l'ora di inizio, tra le due azioni. Ma non
dimenticate le parentesi finali nell'istruzione defer, altrimenti l'azione ''on entry'' avverrà all'uscita e
l'azione on exit non avverrà affatto!

gopl.io/ch5/trace
func bigSlowOperation() {
defer trace("bigSlowOperation")() // non dimenticare le parentesi aggiuntive
// ...molto lavoro...
time.Sleep(10 * time.Second) // simula un funzionamento lento dormendo
}

func trace(msg string) func() { start :=


time.Now() log.Printf("enter
%s", msg)
return func() { log.Printf("exit %s (%s)", msg, time.Since(start)) }
}

Ogni volta che bigSlowOperation viene chiamata, registra la sua entrata e la sua uscita e il tempo trascorso
tra di esse. (Abbiamo usato time.Sleep per simulare un'operazione lenta).

$ go build gopl.io/ch5/trace
$ ./trace
2015/11/18 09:53:26 inserire bigSlowOperation
2015/11/18 09:53:36 uscita bigSlowOperation (10.000589217s)

Le funzioni differite vengono eseguite dopo che le dichiarazioni di ritorno hanno aggiornato le variabili
di risultato della funzione. Poiché una funzione anonima può accedere alle variabili della funzione che la
racchiude, compresi i risultati denominati, una funzione anonima differita può osservare i risultati della
funzione.

Consideriamo la funzione double:

func double(x int) int { restituisce


x+x
}

Dando un nome alla variabile result e aggiungendo un'istruzione defer, possiamo fare in modo che la
funzione stampi i suoi argomenti e i suoi risultati ogni volta che viene chiamata.

www.it-ebooks.info
SEZIONE 5.8. CHIAMATE DI FUNZIONE DIFFERITE 147

func double(x int) (result int) {


defer func() { fmt.Printf("double(%d) = %d\n", x, result) }() return x + x
}

_ = doppio(4)
// Uscita:
// "double(4) = 8"

Questo trucco è eccessivo per una funzione semplice come double, ma può essere utile in funzioni con molte
dichiarazioni di ritorno.
Una funzione anonima differita può anche modificare i valori che la funzione racchiusa restituisce al suo
chiamante:
func triple(x int) (result int) { defer
func() { result += x }() return
double(x)
}

fmt.Println(triple(4)) // "12"

Poiché le funzioni differite non vengono eseguite fino alla fine dell'esecuzione di una funzione,
un'istruzione defer i n un ciclo merita un'attenzione particolare. Il codice seguente potrebbe esaurire i
descrittori di file, poiché nessun file viene chiuso finché non sono stati elaborati tutti i file:
for _, filename := range filenames { f, err
:= os.Open(filename)
if err != nil {
return err
}
defer f.Close() // NOTA: rischioso; potrebbe esaurire i descrittori di file.
// ...elaborare f...
}

Una soluzione è spostare il corpo del ciclo, compresa l'istruzione defer, in un'altra funzione che viene
richiamata a ogni iterazione.
per _, nome file := intervallo nomi file {
if err := doFile(filename); err := nil { return
err
}
}

func doFile(filename string) error { f, err


:= os.Open(filename)
if err != nil {
return err
}
rinviare f.Close()
// ...elaborare f...
}

www.it-ebooks.info
148 CAPITOLO 5. FUNZIONI

L'esempio seguente è un programma di fetch migliorato (§1.5) che scrive la risposta HTTP su un file
locale invece che sullo standard output. Il nome del file deriva dall'ultimo componente del percorso
dell'URL, che viene ottenuto utilizzando la funzione path.Base.
gopl.io/ch5/fetch
// Fetch scarica l'URL e restituisce il file
// nome e lunghezza del file locale.
func fetch(url string) (filename string, n int64, err error) { resp, err :=
http.Get(url)
if err != nil { return
"", 0, err
}
rinviare resp.Body.Close()

local := path.Base(resp.Request.URL.Path) if
local == "/" {
local = "index.html"
}
f, err := os.Create(local) if
err := nil {
restituire "", 0, err
}
n, err = io.Copy(f, resp.Body)
// Chiudere il file, ma preferire l'eventuale errore di
Copy. if closeErr := f.Close(); err == nil {
err = closeErr
}
restituire local, n, err
}

La chiamata differita a resp.Body.Close dovrebbe essere ormai familiare. Si è tentati di usare


una seconda chiamata differita, a f.Close, per chiudere il file locale, ma questo sarebbe sottilmente
sbagliato perché os.Create apre un file per la scrittura, creandolo secondo le necessità. Su molti file
system, in particolare NFS, gli errori di scrittura non vengono segnalati immediatamente, ma possono
essere rimandati fino alla chiusura del file. Il mancato controllo del risultato dell'operazione di chiusura
potrebbe far passare inosservata una grave perdita di dati. Tuttavia, se sia io.Copy che f.Close
falliscono, è preferibile segnalare l'errore di io.Copy, poiché si è verificato per primo ed è più
probabile che ci indichi la causa principale.

Esercizio 5.18: Senza modificarne il comportamento, riscrivete la funzione fetch in modo da usare defer
per chiudere il file scrivibile.

5.9. Panico

Il sistema di tipi di Go individua molti errori in fase di compilazione, ma altri, come l'accesso a un array
fuori dai limiti o il riferimento a un puntatore nil, richiedono controlli in fase di esecuzione. Quando il
runtime Go rileva questi errori, va nel panico.

www.it-ebooks.info
SEZIONE 5.9. PANICO 149

Durante un tipico panico, l'esecuzione normale si interrompe, tutte le chiamate di funzione differite in
quella goroutine vengono eseguite e il programma si blocca con un messaggio di log. Questo messaggio
di log include il valore del panico, che di solito è un messaggio di errore di qualche tipo, e, per ogni
goroutine, una traccia dello stack che mostra lo stack delle chiamate di funzione attive al momento del
panico. Questo messaggio di log spesso contiene informazioni sufficienti per diagnosticare la causa del
problema senza dover eseguire nuovamente il programma, quindi dovrebbe essere sempre incluso in
una segnalazione di bug relativa a un programma in panico.
Non tutti i panic provengono dal runtime. La funzione built-in panic può essere chiamata
direttamente; accetta qualsiasi valore come argomento. Il panico è spesso la cosa migliore da fare
quando si verifica una situazione "impossibile", ad esempio quando l'esecuzione raggiunge un caso che
logicamente non può verificarsi:
switch s := suit(drawCard()); s {
caso "Picche": // ...
caso "Cuori": // ... case
"Diamonds": // ... caso
"Fiori": // ... predefinito:
panic(fmt.Sprintf("seme non valido %q", s)) // Jolly?
}

È buona norma asserire che le precondizioni di una funzione siano valide, ma questo può essere
facilmente fatto in eccesso. A meno che non si possa fornire un messaggio di errore più informativo o
rilevare un errore prima, non ha senso asserire una condizione che il runtime verificherà per voi.
func Reset(x *Buffer) { if x
== nil {
panic("x è nil") // inutile!
}
x.elements = nil
}

Sebbene il meccanismo di panico di Go assomigli alle eccezioni di altri linguaggi, le situazioni in cui
il panico viene utilizzato sono molto diverse. Poiché il panico causa l'arresto del programma, viene
generalmente usato per errori gravi, come un'incoerenza logica nel programma; i programmatori
diligenti considerano ogni arresto come la prova di un bug nel loro codice. In un programma
robusto, gli errori "attesi", quelli che derivano da un input errato, da una configurazione errata o da
un I/O non funzionante, devono essere gestiti con grazia; il modo migliore per affrontarli è usare i
valori di errore.
Consideriamo la funzione regexp.Compile, che compila un'espressione regolare in una forma
efficiente per la corrispondenza. Restituisce un errore se viene chiamata con un modello
malformato, ma il controllo di questo errore è inutile e gravoso se il chiamante sa che una
particolare chiamata non può fallire. In questi casi, è ragionevole che il chiamante gestisca un errore
facendosi prendere dal panico, poiché si ritiene che sia impossibile.
Poiché la maggior parte delle espressioni regolari sono letterali nel codice sorgente del programma, il
pacchetto regexp fornisce una funzione wrapper regexp.MustCompile che esegue questo controllo:
pacchetto regexp

func Compile(expr string) (*Regexp, error) { /* ... */ }

www.it-ebooks.info
150 CAPITOLO 5. FUNZIONI

func MustCompile(expr string) *Regexp { re,


err := Compile(expr)
if err != nil {
panic(err)
}
restituire re
}

La funzione wrapper rende conveniente per i client inizializzare una variabile a livello di pacchetto con
un'espressione regolare compilata, come questa:
var httpSchemeRE = regexp.MustCompile(`^https?:`) // "http:" o "https:"

Naturalmente, MustCompile non dovrebbe essere chiamata con valori di input non attendibili. Il prefisso
Must è una convenzione di denominazione comune per funzioni di questo tipo, come template.Must in
Sezione 4.6.
Quando si verifica un panico, tutte le funzioni differite vengono eseguite in ordine inverso, iniziando da
quelle della funzione più in alto nello stack e procedendo fino a main, come dimostra il programma
seguente:
gopl.io/ch5/defer1
func main() {
f(3)
}

func f(x int) {


fmt.Printf("f(%d)\n", x+0/x) // va in panico se x == 0
deferisce fmt.Printf("defer %d\n", x)
f(x - 1)
}

Quando viene eseguito, il programma stampa quanto segue sullo standard output:
f(3)
f(2)
f(1)
rinviare 1
rinviare 2
rinviare 3

Si verifica un panico durante la chiamata a f(0), causando l'esecuzione delle tre chiamate differite a
fmt.Printf. Quindi il runtime termina il programma, stampando il messaggio di panico e un dump dello
stack s u l flusso di errore standard (semplificato per chiarezza):
panico: errore di runtime: intero diviso per zero
main.f(0)
src/gopl.io/ch5/defer1/defer.go:14
main.f(1)
src/gopl.io/ch5/defer1/defer.go:16
main.f(2)
src/gopl.io/ch5/defer1/defer.go:16

www.it-ebooks.info
SEZIONE 5.10. RECUPERO 151

main.f(3)
src/gopl.io/ch5/defer1/defer.go:16
main.main()
src/gopl.io/ch5/defer1/defer.go:10

Come vedremo tra poco, è possibile per una funzione riprendersi da un panico in modo da non ter- minare il
programma.
Per scopi diagnostici, il pacchetto di runtime consente al programmatore di eseguire il dump dello stack
utilizzando lo stesso macchinario. Rinviando una chiamata a printStack in main,
gopl.io/ch5/defer2
func main() {
rinviare printStack()
f(3)
}
func printStack() { var
buf [4096]byte
n := runtime.Stack(buf[:], false)
os.Stdout.Write(buf[:n])
}

il seguente testo aggiuntivo (ancora una volta semplificato per chiarezza) viene stampato sullo standard
output:
goroutine 1 [in esecuzione]:
main.printStack() src/gopl.io/ch5/defer2/defer.go:20
main.f(0)
src/gopl.io/ch5/defer2/defer.go:27 main.f(1)
src/gopl.io/ch5/defer2/defer.go:29 main.f(2)
src/gopl.io/ch5/defer2/defer.go:29 main.f(3)
src/gopl.io/ch5/defer2/defer.go:29 main.main()
src/gopl.io/ch5/defer2/defer.go:15

Chi ha familiarità con le eccezioni in altri linguaggi potrebbe essere sorpreso dal fatto che runtime.Stack
può stampare informazioni sulle funzioni che sembrano essere già state ''srotolate''. Il meccanismo di
panico di Go esegue le funzioni differite prima di srotolare lo stack.

5.10. Recupero

Rinunciare è di solito la risposta giusta al panico, ma non sempre. Potrebbe essere possibile recuperare
in qualche modo, o almeno ripulire la situazione prima di abbandonare. Ad esempio, un server web che
incontra un problema inaspettato potrebbe chiudere la connessione piuttosto che lasciare il client in
sospeso e, durante lo sviluppo, potrebbe segnalare l'errore anche al client.

www.it-ebooks.info
152 CAPITOLO 5. FUNZIONI

Se la funzione incorporata recover viene chiamata all'interno di una funzione differita e la funzione
che contiene l'istruzione defer è in panico, recover termina lo stato di panico corrente e restituisce il
valore di panico. La funzione che era in panico non continua da dove si era interrotta, ma ritorna
normalmente. Se recover viene chiamato in qualsiasi altro momento, non ha alcun effetto e
restituisce nil.

A titolo esemplificativo, si consideri lo sviluppo di un parser per un linguaggio. Anche quando sembra
funzionare bene, data la complessità del suo lavoro, i bug possono ancora nascondersi in oscuri casi
d'angolo. Preferiremmo che, invece di andare in crash, il parser trasformasse questi problemi in normali
errori di parsing, magari con un messaggio aggiuntivo che esorti l'utente a segnalare il bug.
func Parse(input string) (s *Syntax, err error) { defer
func() {
if p := recover(); p := nil {
err = fmt.Errorf("errore interno: %v", p)
}
}()
// ...parser...
}

La funzione differita in Parse recupera da un panico, utilizzando il valore di panico per costruire un
messaggio di errore; una versione più sofisticata potrebbe includere l'intero stack di chiamate utilizzando
runtime.Stack. La funzione differita assegna quindi al risultato err, che viene restituito al chiamante.

Il recupero indiscriminato dal panico è una pratica dubbia perché lo stato delle variabili di un pacchetto
dopo un panico è raramente ben definito o documentato. Forse un aggiornamento critico di una
struttura dati è stato incompleto, un file o una connessione di rete sono stati aperti ma non chiusi,
oppure un blocco è stato acquisito ma non rilasciato. Inoltre, sostituendo un crash con, ad esempio, una
riga in un file di log, il recupero indiscriminato può far passare inosservati i bug.

Il recupero da un panico all'interno dello stesso pacchetto può aiutare a semplificare la gestione di errori
complessi o inaspettati, ma come regola generale non si dovrebbe tentare di recuperare dal panico di un
altro pacchetto. Le API pubbliche devono segnalare i fallimenti come errori. Allo stesso modo, non si
dovrebbe recuperare da un panico che potrebbe passare attraverso una funzione non gestita dall'utente,
come un callback fornito dal chiamante, poiché non si può ragionare sulla sua sicurezza.

Ad esempio, il pacchetto net/http fornisce un server web che smista le richieste in arrivo a funzioni
di gestione fornite dall'utente. Piuttosto che lasciare che un panico in uno di questi gestori uccida il
processo, il server chiama recover, stampa una traccia dello stack e continua a servire. Questa
soluzione è comoda nella pratica, ma rischia di far perdere risorse o di lasciare il gestore fallito in
uno stato non specificato che potrebbe portare ad altri problemi.

Per tutti questi motivi, è più sicuro recuperare in modo selettivo, se mai. In altre parole, recuperare solo
dai panic che erano destinati a essere recuperati, il che dovrebbe essere raro. Questa intenzione può
essere codificata utilizzando un tipo distinto, non esportato, per il valore del panico e verificando se
il valore restituito da recover ha quel tipo. (In caso affermativo, il panico viene segnalato come errore
ordinario; in caso contrario, si richiama panic con lo stesso valore per riprendere lo stato di panico.

www.it-ebooks.info
SEZIONE 5.10. RECUPERO 153

L'esempio seguente è una variante del programma title che segnala un errore se il documento
HTML contiene più elementi <title>. In tal caso, interrompe la ricorsione chiamando panic con un
valore del tipo speciale bailout.
gopl.io/ch5/titolo3
// soleTitle restituisce il testo del primo elemento titolo non vuoto
// nel doc, e un errore se non ce n'era esattamente uno. func
soleTitle(doc *html.Node) (title string, err error) {
tipo bailout struct{}
deferisci func() {
switch p := recover(); p { case
nil:
// nessun
caso di panico
bailout{}:
// panico "atteso"
err = fmt.Errorf("multiple title elements")
default:
panic(p) // panico inaspettato; continuare a farsi prendere dal panico
}
}()
// Esce dalla ricorsione se troviamo più di un titolo non vuoto. forEachNode(doc, func(n
*html.Node) {
se n.Type == html.ElementNode && n.Data == "title" && n.FirstChild != nil
{
if title != "" {
panic(bailout{}) // elementi multipli del titolo
}
titolo = n.FirstChild.Data
}
}, nil)
if title == "" {
return "", fmt.Errorf("nessun elemento del titolo")
}
restituire titolo, nil
}

La funzione di gestione differita chiama recover, controlla il valore di panico e segnala un errore
ordinario se il valore era bailout{}. Tutti gli altri valori non nulli indicano un panico inatteso, nel
qual caso il gestore chiama panic con quel valore, annullando l'effetto di recover e riprendendo lo
stato di panico originale. (Questo esempio viola in qualche modo il nostro consiglio di non usare il
panico per gli errori ''attesi'', ma fornisce un'illustrazione compatta della meccanica).
Da alcune condizioni non è possibile recuperare. L'esaurimento della memoria, ad esempio, fa sì che il
runtime Go interrompa il programma con un errore fatale.
Esercizio 5.19: Usate panic e recover per scrivere una funzione che non contiene dichiarazioni di
ritorno, ma che restituisce un valore non nullo.

www.it-ebooks.info
Questa pagina è stata lasciata intenzionalmente in bianco

www.it-ebooks.info
6
Metodi

Dall'inizio degli anni '90, la programmazione orientata agli oggetti (OOP) è stata il paradigma di
programmazione dominante nell'industria e nell'istruzione, e quasi tutti i linguaggi più diffusi sviluppati
da allora ne hanno incluso il supporto. Go non fa eccezione.
Sebbene non esista una definizione universalmente accettata di programmazione orientata agli oggetti,
per i nostri scopi, un oggetto è semplicemente un valore o una variabile che ha dei metodi, e un metodo è
una funzione associata a un particolare tipo. Un programma orientato agli oggetti è un programma che
utilizza i metodi per esprimere le proprietà e le operazioni di ogni struttura dati, in modo che i client
non debbano accedere direttamente alla rappresentazione dell'oggetto.
Nei capitoli precedenti, abbiamo utilizzato regolarmente i metodi della libreria standard, come il metodo
Metodo Seconds di tipo time.Duration:
const day = 24 * time.Hour fmt.Println(day.Seconds())
// "86400"

e abbiamo definito un nostro metodo nella Sezione 2.5, un metodo String per il tipo Celsius:
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }

In questo capitolo, il primo di due sulla programmazione orientata agli oggetti, mostreremo come
definire e utilizzare i metodi in modo efficace. Verranno inoltre illustrati due principi chiave della
programmazione orientata agli oggetti, l'incapsulamento e la composizione.

6.1. Metodo Dichiarazioni

Un metodo viene dichiarato con una variante della dichiarazione di funzione ordinaria, in cui un
parametro aggiuntivo compare prima del nome della funzione. Il parametro associa la funzione al tipo di
parametro.

155

www.it-ebooks.info
156 CAPITOLO 6. METODI

Scriviamo il nostro primo metodo in un semplice pacchetto per la geometria piana:


gopl.io/ch6/geometria
pacchetto geometria

importare

"matematica"

tipo Punto struct{ X, Y float64 }

// funzione tradizionale
func Distance(p, q Point) float64 { return
math.Hypot(q.X-p.X, q.Y-p.Y)
}

// la stessa cosa, ma come metodo del tipo Point func (p Point)


Distance(q Point) float64 {
restituire math.Hypot(q.X-p.X, q.Y-p.Y)
}

Il parametro aggiuntivo p è chiamato ricevitore del metodo, un'eredità dei primi linguaggi orientati agli
oggetti che descrivevano la chiamata di un metodo come "l'invio di un messaggio a un oggetto".

In Go, non si usa un nome speciale come this o self per il ricevitore; si scelgono i nomi dei
ricevitori come per qualsiasi altro parametro. Poiché il nome del ricevitore sarà usato
frequentemente, è una buona idea scegliere qualcosa di breve e che sia coerente tra i vari metodi. Una
scelta comune è la prima lettera del nome del tipo, come p per Point.

In una chiamata di metodo, l'argomento del destinatario compare prima del nome del metodo. Ciò
corrisponde alla dichiarazione, in cui il parametro del destinatario compare prima del nome del metodo.
p := Punto{1, 2}
q := Punto{4, 6}
fmt.Println(Distanza(p, q)) // "5", chiamata di funzione
fmt.Println(p.Distance(q)) // "5", chiamata di metodo

Non c'è conflitto tra le due dichiarazioni di funzioni chiamate Distance di cui sopra. La prima dichiara
una funzione a livello di pacchetto chiamata geometry.Distance. La seconda dichiara un metodo di tipo
Point, quindi il suo nome è Point.Distance.

L'espressione p.Distance è chiamata selettore, perché seleziona il metodo Distance appropriato per
il ricevitore p di tipo Point. I selettori sono usati anche per selezionare campi di tipi struct, come in p.X.
Poiché metodi e campi abitano lo stesso spazio dei nomi, dichiarare un metodo X sul tipo struct Point
sarebbe ambiguo e il compilatore lo rifiuterebbe.

Poiché ogni tipo ha il proprio spazio dei nomi per i metodi, possiamo usare il nome Distanza per altri
metodi, purché appartengano a tipi diversi. Definiamo un tipo Sentiero che rappresenti una sequenza di
segmenti di linea e diamogli anche un metodo Distanza.
// Un percorso è un viaggio che collega i punti con linee rette. type Percorso
[]Punto

www.it-ebooks.info
SEZIONE 6.1. DICHIARAZIONI DI METODO 157

// Distance restituisce la distanza percorsa lungo il percorso. func


(path Path) Distance() float64 {
somma := 0.0
per i := percorso
dell'intervallo { se
i >0{
somma += percorso[i-1].Distanza(percorso[i])
}
}
restituire la somma
}

Path è un tipo slice con nome, non un tipo struct come Point, ma possiamo comunque definirne i
metodi. Permettendo di associare metodi a qualsiasi tipo, Go è diverso da molti altri linguaggi orientati
agli oggetti. Spesso è conveniente definire comportamenti aggiuntivi per tipi semplici come numeri,
stringhe, slice, mappe e talvolta anche funzioni. I metodi possono essere dichiarati su qualsiasi tipo
definito nello stesso pacchetto, purché il suo tipo sottostante non sia un puntatore o un'interfaccia.
I due metodi Distance hanno tipi diversi. Non sono affatto correlati tra loro, anche se
Path.Distance utilizza internamente Point.Distance per calcolare la lunghezza di ogni segmento
che collega punti adiacenti.
Chiamiamo il nuovo metodo per calcolare il perimetro di un triangolo rettangolo:

perim := Percorso{
{1, 1},
{5, 1},
{5, 4},
{1, 1},
}
fmt.Println(perim.Distance()) // "12"

Nelle due chiamate precedenti a metodi denominati Distance, il compilatore determina quale funzione
chiamare in base al nome del metodo e al tipo del destinatario. Nella prima, percorso[i-1] ha il
tipo Point, quindi viene richiamata Point.Distance; nella seconda, perim ha il tipo Path,
quindi viene richiamata Path.Distance.
Tutti i metodi di un dato tipo devono avere nomi univoci, ma tipi diversi possono usare lo stesso nome
per un metodo, come i metodi Distance per Point e Path; non è necessario qualificare i nomi delle funzioni
(per esempio, PathDistance) per disambiguare. Ecco il primo vantaggio dell'uso dei metodi rispetto alle
funzioni ordinarie: i nomi dei metodi possono essere più brevi. Il vantaggio è maggiore per le chiamate
che provengono dall'esterno del pacchetto, poiché possono usare il nome più corto e omettere il nome
del pacchetto:
importare "gopl.io/ch6/geometria"

perim := geometry.Path{{1, 1}, {5, 1}, {5, 4}, {1, 1}}


fmt.Println(geometry.PathDistance(perim)) // "12", funzione autonoma
fmt.Println(perim.Distance()) // "12", metodo di geometry.Path

www.it-ebooks.info
158 CAPITOLO 6. METODI

6.2. Metodi con un ricevitore Pointer

Poiché la chiamata di una funzione crea una copia di ogni valore dell'argomento, se una funzione ha
bisogno di aggiornare una variabile, o se un argomento è così grande che vogliamo evitare di copiarlo,
dobbiamo passare l'indirizzo della variabile usando un puntatore. Lo stesso vale per i metodi che devono
aggiornare la variabile ricevente: li attacchiamo al tipo di puntatore, come *Point.
func (p *Punto) ScaleBy(fattore float64) {
p.X *= fattore
p.Y *= fattore
}

Il nome di questo metodo è (*Punto).ScaleBy. Le parentesi sono necessarie; senza di esse,


l'espressione verrebbe analizzata come *(Point.ScaleBy).
In un programma realistico, le convenzioni impongono che se un metodo di Point ha un ricevitore
di puntatori, allora tutti i metodi di Point dovrebbero avere un ricevitore di puntatori, anche quelli che
non ne hanno strettamente bisogno. Abbiamo infranto questa regola per Point in modo da poter
mostrare entrambi i tipi di metodo.
I tipi denominati (Point) e i puntatori a essi (*Point) sono gli unici tipi che possono comparire in una
dichiarazione di ricevitore. Inoltre, per evitare ambiguità, non sono consentite dichiarazioni di metodi
su tipi denominati che sono a loro volta tipi puntatori:
tipo P *int
func (P) f() { /* ... */ } // errore di compilazione: tipo di ricevitore non valido

Il metodo (*Point).ScaleBy può essere richiamato fornendo un ricevitore *Point, in questo modo:
r := &Punto{1, 2} r.ScaleBy(2)
fmt.Println(*r) // "{2, 4}"

o questo:
p := Punto{1, 2}
pptr := &p
pptr.ScaleBy(2)
fmt.Println(p) // "{2, 4}"

o questo:
p := Punto{1, 2}
(&p).ScaleBy(2) fmt.Println(p)
// "{2, 4}"

Ma gli ultimi due casi sono difficili da risolvere. Fortunatamente, il linguaggio ci aiuta in questo caso. Se
il ricevitore p è una variabile di tipo Point ma il metodo richiede un ricevitore *Point, possiamo usare
questa abbreviazione:
p.ScaleBy(2)

e il compilatore eseguirà un &p implicito sulla variabile. Questo funziona solo per le variabili,
compresi i campi struct come p.X e gli elementi di array o slice come perim[0]. Non è possibile
chiamare una variabile
*su un ricevitore Point non indirizzabile, perché non c'è modo di ottenere l'indirizzo del ricevitore Point.

www.it-ebooks.info
SEZIONE 6.2. METODI CON UN RICEVITORE DI PUNTATORI 159

indirizzo di un valore temporaneo.


Point{1, 2}.ScaleBy(2) // errore di compilazione: non può prendere l'indirizzo di un letterale
di Point

Ma possiamo chiamare un metodo Point come Point.Distance con un ricevitore *Point, perché
esiste un modo per ottenere il valore dall'indirizzo: basta caricare il valore puntato dal ricevitore. Il
compilatore inserisce un'operazione implicita * per noi. Queste due chiamate di funzione sono
equivalenti:
pptr.Distanza(q) (*pptr).Distanza(q)

Riassumiamo ancora una volta questi tre casi, poiché sono un punto di confusione frequente. In ogni
espressione di chiamata di metodo valida, esattamente una di queste tre affermazioni è vera.
O l'argomento del ricevitore ha lo stesso tipo del parametro del ricevitore, ad esempio entrambi hanno
tipo T o entrambi hanno tipo *T:
Point{1, 2}.Distance(q) // Punto
pptr.ScaleBy(2) // Punto

Oppure l'argomento del ricevitore è una variabile di tipo T e il parametro del ricevitore è di tipo *T. Il
compilatore prende implicitamente l'indirizzo della variabile:
p.ScaleBy(2) // implicito (&p)

Oppure l'argomento del ricevitore è di tipo *T e il parametro del ricevitore è di tipo T. Il compilatore
dereferenzia implicitamente il ricevitore, in altre parole, carica il valore:
pptr.Distance(q) // implicito (*pptr)

Se tutti i metodi di un tipo chiamato T hanno un tipo ricevitore di T stesso (non *T), è sicuro copiare
istanze di quel tipo; la chiamata di uno qualsiasi dei suoi metodi crea necessariamente una copia.
Ad esempio, i valori di time.Duration vengono copiati liberamente, anche come argomenti di
funzioni. Ma se un metodo ha un puntatore ricevitore, si dovrebbe evitare di copiare istanze di T, perché
ciò potrebbe violare gli invarianti interni. Ad esempio, copiare un'istanza di bytes.Buffer farebbe sì
che l'originale e la copia facciano da alias (§2.3.2) allo stesso array di byte sottostante. Le successive
chiamate di metodo avrebbero effetti imprevedibili.

6.2.1. Nil Isa Valore valido del ricevitore

Così come alcune funzioni ammettono puntatori nil come argomenti, lo stesso fanno alcuni metodi per i
loro destinatari, soprattutto se nil è un valore nullo significativo del tipo, come nel caso di mappe e
slices. In questo semplice elenco collegato di interi, nil rappresenta l'elenco vuoto:
// Un IntList è un elenco collegato di numeri interi.
// Un *IntList nullo rappresenta l'elenco vuoto.
type IntList struct {
Valore int
Coda *IntList
}

www.it-ebooks.info
160 CAPITOLO 6. METODI

// Sum restituisce la somma degli elementi della


lista. func (list *IntList) Sum() int {
if list == nil {
return 0
}
restituire list.Value + list.Tail.Sum()
}

Quando si definisce un tipo i cui metodi ammettono nil come valore ricevitore, vale la pena di indicarlo
esplicitamente nel commento della documentazione, come abbiamo fatto sopra.
Ecco una parte della definizione del tipo Values dal pacchetto net/url:
rete/url
url del pacchetto
// Valori mappa una chiave stringa in un elenco di valori.
tipo Valori map[string][]string
// Get restituisce il primo valore associato alla chiave data,
// o "" se non ci sono.
func (v Values) Get(key string) string { if vs
:= v[key]; len(vs) > 0 {
restituire vs[0]
}
restituire ""
}
// Aggiunge il valore alla chiave.
// Si aggiunge a qualsiasi valore esistente associato alla chiave. func
(v Values) Add(key, value string) {
v[chiave] = append(v[chiave], valore)
}

Espone la sua rappresentazione come mappa, ma fornisce anche metodi per semplificare l'accesso alla
mappa, i cui valori sono fette di stringhe: è una multimappa. I suoi clienti possono usare i suoi operatori
intrinseci (make, slice literals, m[key] e così via), o i suoi metodi, o entrambi, come preferiscono:
gopl.io/ch6/urlvalues
m := url.Values{"lang": {"en"}} // costruzione diretta
m.Add("item", "1")
m.Add("item", "2")
fmt.Println(m.Get("lang")) // "en"
fmt.Println(m.Get("q")) // "" fmt.Println(m.Get("item")) //
"1" (primo valore)
fmt.Println(m["item"]) // "[1 2]" ( accesso diretto alla
mappa)
m = nil fmt.Println(m.Get("item")) //
""
m.Add("item", "3") // panico: assegnazione a una voce nella mappa nil

Nella chiamata finale a Get, il ricevitore nil si comporta come una mappa vuota. Avremmo potuto
scriverlo equivalentemente come Values(nil).Get("item")), ma nil.Get("item") non verrà
compilato perché

www.it-ebooks.info
SEZIONE 6.3. COMPOSIZIONE DI TIPI MEDIANTE INCORPORAZIONE DI 161
STRUCT

il tipo di nil non è stato determinato. Al contrario, la chiamata finale ad Add va in panico quando
cerca di aggiornare una mappa nil.
Poiché url.Values è un tipo di mappa e una mappa fa riferimento alle sue coppie chiave/valore in modo
indiretto, tutti gli aggiornamenti e le cancellazioni che url.Values.Add apporta agli elementi della
mappa sono visibili al chiamante. Tuttavia, come per le funzioni ordinarie, qualsiasi modifica apportata
da un metodo al riferimento stesso, come impostarlo a nil o farlo riferire a una diversa struttura di dati
della mappa, non si rifletterà sul chiamante.

6.3. Comporre tipi tramite l'incorporazione di Struct

Consideriamo il tipo ColoredPoint:


gopl.io/ch6/punto colorato
importare "image/color"

tipo Punto struct{ X, Y float64 } tipo


Punto colorato struct {
Punto
Colore color.RGBA
}

Avremmo potuto definire ColoredPoint come una struct di tre campi, ma invece abbiamo
incorporato un Point per fornire i campi X e Y. Come abbiamo visto nella Sezione 4.4.3,
l'incorporazione ci permette di prendere una scorciatoia sintattica per definire un ColoredPoint che
contiene tutti i campi di Point, più altri. Se si vuole, si possono selezionare i campi di ColoredPoint
che sono stati forniti dal Punto incorporato, senza menzionare il Punto:
var cp Punto colorato
cp.X = 1
fmt.Println(cp.Point.X) // "1"
cp.Point.Y = 2
fmt.Println(cp.Y) // "2"

Un meccanismo simile si applica ai metodi di Point. Possiamo chiamare i metodi del campo incorporato
Point utilizzando un ricevitore di tipo ColoredPoint, anche se ColoredPoint non ha metodi
dichiarati:
rosso := color.RGBA{255, 0, 0, 255}
blu := color.RGBA{0, 0, 255, 255}
var p = ColoredPoint{Point{1, 1}, rosso} var q
= ColoredPoint{Point{5, 4}, blu}
fmt.Println(p.Distance(q.Point)) // "5"
p.ScaleBy(2)
q.ScaleBy(2) fmt.Println(p.Distance(q.Point)) //
"10"

I metodi di Point sono stati promossi a ColoredPoint. In questo modo, l'incorporamento consente di
costruire tipi complessi con molti metodi, attraverso la composizione di diversi campi, ciascuno dei quali
è

www.it-ebooks.info
162 CAPITOLO 6. METODI

fornendo alcuni metodi.


Chi ha familiarità con i linguaggi orientati agli oggetti basati sulle classi può essere tentato di
considerare Point come una classe base e ColoredPoint come una sottoclasse o una classe derivata, o di
interpretare la relazione tra questi tipi come se un ColoredPoint ''fosse un'' Point. Ma sarebbe un
errore. Notate le chiamate a Distance di cui sopra. Distance ha un parametro di tipo Point e q non è un
Point, quindi anche se q ha un campo incorporato di quel tipo, dobbiamo selezionarlo esplicitamente. Il
tentativo di passare q sarebbe un errore:
p.Distance(q) // errore di compilazione: impossibile utilizzare q (ColoredPoint) come Point

Un Punto colorato non è un Punto, ma "ha un" Punto e ha due metodi aggiuntivi Dis- tanza e ScalaPer
promossi da Punto. Se si preferisce pensare in termini di implementazione, il campo incorporato
istruisce il compilatore a generare metodi wrapper aggiuntivi che eliminano i metodi dichiarati,
equivalenti a questi:
func (p ColoredPoint) Distance(q Point) float64 { return
p.Point.Distance(q)
}

func (p *PuntoColorato) ScaleBy(fattore float64) {


p.Point.ScaleBy(fattore)
}

Quando Point.Distance viene richiamato dal primo di questi metodi wrapper, il suo valore di
ricezione è p.Point, non p, e non c'è modo per il metodo di accedere al ColoredPoint in cui il
punto è incorporato.

Il tipo di un campo anonimo può essere un puntatore a un tipo denominato, nel qual caso i campi
e i metodi sono promossi indirettamente dall'oggetto puntato. L'aggiunta di un altro livello di indi-
rizzo ci permette di condividere strutture comuni e di variare dinamicamente le relazioni tra gli oggetti.
La dichiarazione di ColoredPoint qui sotto incorpora un *Point:
tipo ColoredPoint struct {
*Punto
Colore color.RGBA
}

p := Punto colorato{&Punto{1, 1}, rosso}


q := Punto colorato{&Punto{5, 4}, blu}
fmt.Println(p.Distance(*q.Point)) // "5"
q.Point = p.Point // p e q ora condividono lo stesso punto
p.ScaleBy(2)
fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"

Un tipo struct può avere più di un campo anonimo. Se avessimo dichiarato ColoredPoint come
tipo ColoredPoint struct {
Punto
color.RGBA
}

www.it-ebooks.info
SEZIONE 6.3. COMPOSIZIONE DI TIPI MEDIANTE INCORPORAZIONE DI 163
STRUCT

allora un valore di questo tipo avrà tutti i metodi di Point, tutti i metodi di RGBA e qualsiasi altro
metodo dichiarato direttamente su ColoredPoint. Quando il compilatore risolve un selettore come
p.ScaleBy in un metodo, cerca innanzitutto un metodo direttamente dichiarato chiamato ScaleBy, poi i
metodi promossi una volta dai campi incorporati di ColoredPoint, poi i metodi promossi due volte
dai campi incorporati in Point e RGBA e così via. Il compilatore segnala un errore se il selettore è ambiguo
perché due metodi sono stati promossi dallo stesso rango.

I metodi possono essere dichiarati solo sui tipi denominati (come Point) e sui puntatori ad essi (*Point),
ma grazie all'embedding è possibile e talvolta utile che anche i tipi struct senza nome abbiano dei metodi.

Ecco un bel trucco per illustrarlo. Questo esempio mostra parte di una semplice cache implementata
utilizzando due variabili a livello di pacchetto, un mutex (§9.2) e la mappa che esso custodisce:

var (
mu sync.Mutex // guardie mapping mapping =
make(map[string]string)
)

func Lookup(key string) string { mu.Lock()


v := mapping[chiave]
mu.Unlock() return v
}

La versione seguente è funzionalmente equivalente, ma raggruppa le due variabili correlate in un'unica


variabile a livello di pacchetto, cache:

var cache = struct { sync.Mutex


mappatura map[string]string
} {
mapping: make(map[string]string),
}

func Lookup(key string) string { cache.Lock()


v := cache.mapping[key] cache.Unlock()
restituire v
}

La nuova variabile dà nomi più espressivi alle variabili relative alla cache e, poiché il campo sync.Mutex è
incorporato al suo interno, i suoi metodi Lock e Unlock sono promossi al tipo struct senza nome,
consentendo di bloccare la cache con una sintassi autoesplicativa.

www.it-ebooks.info
164 CAPITOLO 6. METODI

6.4. Valori del metodo ed espressioni di

Di solito selezioniamo e chiamiamo un metodo nella stessa espressione, come in p.Distance(), ma è


possibile separare queste due operazioni. Il selettore p.Distanza produce un valore di metodo, una
funzione che lega un metodo (Punto.Distanza) a uno specifico valore di ricevitore p. Questa funzione
può quindi essere invocata senza un valore di ricevitore; ha bisogno solo degli argomenti non destinatari.
p := Punto{1, 2}
q := Punto{4, 6}
distanzaDaP := p.Distanza // valore del
metodo fmt.Println(distanceFromP(q)) // "5"
var origine Punto // {0, 0}
fmt.Println(distanzaDaP(origine)) // "2.23606797749979", ;5
scaleP := p.ScaleBy // valore del metodo
scaleP(2) // p diventa (2, 4)
scalaP(3) // allora (6, 12)
scaleP(10) // allora (60, 120)

I valori di metodo sono utili quando l'API di un pacchetto richiede un valore di funzione e il
comportamento desiderato dal client per tale funzione è quello di chiamare un metodo su un
ricevitore specifico. Ad esempio, la funzione time.AfterFunc richiama un valore di funzione dopo
un ritardo specificato. Questo programma la utilizza per lanciare il razzo r dopo 10 secondi:
type Rocket struct { /* ... */ }
func (r *Rocket) Launch() { /* ... */ }
r := nuovo(Rocket)
time.AfterFunc(10 * time.Second, func() { r.Launch() })

La sintassi del valore del metodo è più breve:


time.AfterFunc(10 * time.Second, r.Launch)

Il valore del metodo è correlato all'espressione del metodo. Quando si chiama un metodo, a differenza di
una normale funzione, è necessario fornire il destinatario in modo speciale, utilizzando la sintassi del
selettore. Un'espressione di metodo, scritta T.f o (*T).f dove T è un tipo, produce un valore di funzione
con un primo parametro regolare che prende il posto del ricevitore, in modo da poter essere chiamato
nel modo consueto.
p := Punto{1, 2}
q := Punto{4, 6}
distanza := Punto.Distanza // espressione del
metodo fmt.Println(distance(p, q)) // "5"
fmt.Printf("%T\n", distanza) // "func(Punto, Punto) float64"
scala := (*Punto).ScaleBy scala(&p,
2)
fmt.Println(p) // "{2 4}" fmt.Printf("%T\n",
scala) // "func(*Punto, float64)"

Le espressioni di metodo possono essere utili quando si ha bisogno di un valore che rappresenti una
scelta tra diversi metodi appartenenti allo stesso tipo, in modo da poter chiamare il metodo prescelto con
molte

www.it-ebooks.info
SEZIONE 6.5. ESEMPIO: TIPO DI VETTORE DI BIT 165

diversi ricevitori. Nell'esempio seguente, la variabile op rappresenta il metodo di addizione o di


sottrazione di tipo Point e Path.TranslateBy lo richiama per ogni punto del percorso:
tipo Punto struct{ X, Y float64 }

func (p Punto) Aggiungi(q Punto) Punto { restituisce Punto{p.X + q.X, p.Y + q.Y} } func (p
Punto) Sub(q Punto) Punto { restituisce Punto{p.X - q.X, p.Y - q.Y} }

tipo Percorso []Punto

func (percorso Sentiero) TranslateBy(offset Punto, add bool) { var


op func(p, q Punto) Punto
se aggiungere {
op = Point.Add
} else {
op = Point.Sub
}
per i := intervallo percorso {
// Chiamare o path[i].Add(offset) o path[i].Sub(offset). path[i] =
op(path[i], offset)
}
}

6.5. Esempio: Vettore di bit Tipo

Gli insiemi in Go sono solitamente implementati come map[T]bool, dove T è il tipo di elemento. Un
insieme rappresentato da una mappa è molto flessibile ma, per alcuni problemi, una rappresentazione
specializzata può superarlo. Ad esempio, in domini come l'analisi del flusso di dati, dove gli elementi
dell'insieme sono piccoli numeri interi non negativi, gli insiemi hanno molti elementi e le operazioni
sugli insiemi come l'unione e l'intersezione sono comuni, un vettore di bit è ideale.
Un vettore di bit utilizza una serie di valori interi senza segno o "parole", ogni bit delle quali rappresenta un
possibile elemento dell'insieme. L'insieme contiene i se l'i-esimo bit è impostato. Il programma seguente
mostra un semplice tipo di vettore di bit con tre metodi:
gopl.io/ch6/intset
// Un IntSet è un insieme di piccoli numeri interi non negativi.
// Il suo valore zero rappresenta l'insieme vuoto.
type IntSet struct {
parole []uint64
}

// Has segnala se l'insieme contiene il valore non negativo x. func (s *IntSet)


Has(x int) bool {
parola, bit := x/64, uint(x%64)
return word < len(s.words) && s.words[word]&(1<<bit) != 0
}

www.it-ebooks.info
166 CAPITOLO 6. METODI

// Add aggiunge il valore non negativo x all'insieme. func (s


*IntSet) Add(x int) {
word, bit := x/64, uint(x%64) for
word >= len(s.words) {
s.parole = append(s.parole, 0)
}
s.words[word] |= 1 << bit
}

// UnionWith imposta s sull'unione di s e t. func (s


*IntSet) UnionWith(t *IntSet) {
per i, tword := range t.words { if i
< len(s.words) {
s.words[i] |= tword
} else {
s.parole = append(s.parole, tword)
}
}
}

Poiché ogni parola ha 64 bit, per individuare il bit per x si usa il quoziente x/64 come indice della parola e
il resto x%64 come indice del bit all'interno della parola. L'operazione UnionWith utilizza l'operatore OR
bitwise | per calcolare l'unione di 64 elementi alla volta. (La scelta delle parole a 64 bit sarà esaminata
nell'Esercizio 6.5).

Questa implementazione manca di molte caratteristiche desiderabili, alcune delle quali sono proposte
come esercizi qui di seguito, ma una è difficile da evitare: un modo per stampare un IntSet come stringa.
Diamogli un metodo String, come abbiamo fatto con Celsius nella Sezione 2.5:

// Stringa restituisce l'insieme come una stringa della forma "{1 2


3}". func (s *IntSet) String() string {
var buf bytes.Buffer buf.WriteByte('{')
per i, word := range s.words { if
word == 0 {
continuare
}
per j := 0; j < 64; j++ {
if word&(1<<uint(j)) != 0 { if
buf.Len() > len("{") {
buf.WriteByte(' ' )
}
fmt.Fprintf(&buf, "%d", 64*i+j)
}
}
}
buf.WriteByte('}')
return buf.String()
}

www.it-ebooks.info
SEZIONE 6.5. ESEMPIO: TIPO DI VETTORE DI BIT 167

Notate la somiglianza del metodo String qui sopra con intsToString nella Sezione 3.5.4; bytes.Buffer
è spesso usato in questo modo nei metodi String. Il pacchetto fmt tratta i tipi con un metodo String
in modo particolare, in modo che i valori di tipi complicati possano essere visualizzati in modo
semplice. Invece di stampare la rappresentazione grezza del valore (una struct in questo caso), fmt
chiama il metodo String. Il meccanismo si basa sulle interfacce e sulle asserzioni di tipo, che
verranno spiegate nel Capitolo 7.
Ora possiamo mostrare IntSet in azione:
var x, y IntSet
x.Add(1) x.Add(144)
x.Add(9)
fmt.Println(x.String()) // "{1 9 144}"
y.Add(9)
y.Add(42)
fmt.Println(y.String()) // "{9 42}"
x.UnionWith(&y)
fmt.Println(x.String()) // "{1 9 42 144}" fmt.Println(x.Has(9),
x.Has(123)) // "vero falso"
Un'avvertenza: abbiamo dichiarato String e Has come metodi del tipo puntatore *IntSet non per
necessità, ma per coerenza con gli altri due metodi, che hanno bisogno di un ricevitore puntatore perché
assegnano a s.words. Di conseguenza, un valore IntSet non ha un metodo String, il che può portare a
sorprese come questa:
fmt.Println(&x) // "{1 9 42 144}"
fmt.Println(x.String()) // " {1 9 42 144}"
fmt.Println(x) // "{[4398046511618 0 65536]}"

Nel primo caso, stampiamo un puntatore *IntSet, che ha un metodo String. Nel secondo caso,
chiamiamo String() su una variabile IntSet; il compilatore inserisce l'operazione implicita &,
dandoci un puntatore che ha il metodo String. Ma nel terzo caso, poiché il valore IntSet non ha un
metodo String, fmt.Println stampa invece la rappresentazione della struct. È importante non
dimenticare l'operatore &. Rendere String un metodo di IntSet, e non di *IntSet, potrebbe essere
una buona idea, ma questo è un giudizio da dare caso per caso.
Esercizio 6.1: Implementare questi metodi aggiuntivi:
func (*IntSet) Len() int // restituisce il numero di elementi
func (*IntSet) Remove(x int) // rimuove x dall'insieme
func (*IntSet) Clear() // rimuove tutti gli elementi dall'insieme
func (*IntSet) Copy() *IntSet // restituisce una copia dell'insieme

Esercizio 6.2: Definire un metodo variadico (*IntSet).AddAll(...int) che consenta di


aggiungere un elenco di valori, come s.AddAll(1, 2, 3).
Esercizio 6.3: (*IntSet).UnionWith calcola l'unione di due insiemi usando |, l'operatore OR bitwise
word-paral- lel. Implementare i metodi IntersectWith, DifferenceWith e Sym- metricDifference per le
corrispondenti operazioni sugli insiemi. (La differenza simmetrica di due

www.it-ebooks.info
168 CAPITOLO 6. METODI

contiene gli elementi presenti in un insieme o nell'altro, ma non in entrambi).


Esercizio 6.4: Aggiungere un metodo Elems che restituisca una slice contenente gli elementi dell'insieme,
adatta all'iterazione con un ciclo range.
Esercizio 6.5: Il tipo di ogni parola utilizzata da IntSet è uint64, ma l'aritmetica a 64 bit potrebbe
essere inefficiente su una piattaforma a 32 bit. Modificate il programma per utilizzare il tipo uint,
che è il tipo di intero senza segno più efficiente per la piattaforma. Invece di dividere per 64, si
definisca una costante che contenga la dimensione effettiva di uint in bit, 32 o 64. Si può usare il
metodo, forse troppo semplice, del "numero di bit". A questo scopo si può usare l'espressione forse
troppo astuta 32 << (^uint(0) >> 63).

6.6. Incapsulamento

Una variabile o un metodo di un oggetto si dice incapsulato se è inaccessibile ai client dell'oggetto.


L'incapsulamento, talvolta chiamato information hiding, è un aspetto chiave della programmazione
orientata agli oggetti.
Go ha un solo meccanismo per controllare la visibilità dei nomi: gli identificatori in maiuscolo sono
esportati dal pacchetto in cui sono definiti, mentre i nomi non in maiuscolo non lo sono. Lo stesso
meccanismo che limita l'accesso ai membri di un package limita anche l'accesso ai campi di una struct o
ai metodi di un tipo. Di conseguenza, per incapsulare un oggetto, dobbiamo renderlo una struct.
Questo è il motivo per cui il tipo IntSet della sezione precedente è stato dichiarato come tipo struct,
anche se ha un solo campo:
tipo IntSet struct {
parole []uint64
}

Potremmo invece definire IntSet come tipo slice come segue, anche se ovviamente dovremmo sostituire
ogni occorrenza di s.words con *s nei suoi metodi:
tipo IntSet []uint64

Anche se questa versione di IntSet sarebbe essenzialmente equivalente, permetterebbe ai client di altri
pacchetti di leggere e modificare direttamente lo slice. In altre parole, mentre l'espressione
*s può essere utilizzato in qualsiasi pacchetto, s.words può apparire solo nel pacchetto che definisce
IntSet.

Un'altra conseguenza di questo meccanismo basato sui nomi è che l'unità di incapsulamento è il
pacchetto, non il tipo come in molti altri linguaggi. I campi di un tipo struct sono visibili a tutto il codice
dello stesso pacchetto. Non fa differenza se il codice appare in una funzione o in un metodo.
L'incapsulamento offre tre vantaggi. In primo luogo, poiché i client non possono modificare
direttamente le variabili dell'oggetto, è necessario ispezionare un minor numero di istruzioni per
comprendere i possibili valori di tali variabili.

www.it-ebooks.info
SEZIONE 6.6. INCAPSULAZIONE 169

In secondo luogo, nascondendo i dettagli dell'implementazione si evita che i client dipendano da


elementi che potrebbero cambiare, dando al progettista una maggiore libertà di evolvere
l'implementazione senza rompere la compatibilità dell'API.
A titolo di esempio, si consideri il tipo bytes.Buffer. Viene spesso utilizzato per accumulare stringhe
molto brevi, quindi è un'ottimizzazione vantaggiosa riservare un po' di spazio in più nell'oggetto per
evitare l'allocazione di memoria in questo caso comune. Poiché Buffer è un tipo struct, questo spazio
assume la forma di un campo extra di tipo [64]byte con un nome non maiuscolo. Quando questo campo
è stato aggiunto, poiché non era esportato, i clienti di Buffer al di fuori del pacchetto byte non erano a
conoscenza di alcun cambiamento, se non il miglioramento delle prestazioni. Buffer e il suo metodo
Grow sono mostrati di seguito, semplificati per chiarezza:
type Buffer struct {
buf []byte
iniziale [64]byte
/* ... */
}

// Se necessario, Grow espande la capacità del buffer,


// per garantire spazio per altri n byte. [...] func (b *Buffer)
Grow(n int) {
if b.buf == nil {
b.buf = b.initial[:0] // utilizza inizialmente lo spazio preallocato
}
se len(b.buf)+n > cap(b.buf) {
buf := make([]byte, b.Len(), 2*cap(b.buf) + n) copy(buf,
b.buf)
b.buf = buf
}
}

Il terzo vantaggio dell'incapsulamento, in molti casi il più importante, è che impedisce ai client di
impostare le variabili di un oggetto in modo arbitrario. Poiché le variabili dell'oggetto possono essere
impostate solo da funzioni dello stesso pacchetto, l'autore del pacchetto può garantire che tutte le
funzioni mantengano gli invarianti interni dell'oggetto. Per esempio, il tipo Counter qui sotto permette ai
client di incrementare il contatore o di azzerarlo, ma non di impostarlo su un valore arbitrario:
type Contatore struct { n int }

func (c *Counter) N() int { return c.n }


func (c *Counter) Increment() { c.n++ } func (c
*Counter) Reset() { c.n = 0 }

Le funzioni che si limitano ad accedere o a modificare i valori interni di un tipo, come i metodi del
tipo Logger del pacchetto log, riportati di seguito, sono chiamate getter e setter. Tuttavia, quando si
nomina un metodo getter, di solito si omette il prefisso Get. Questa preferenza per la brevità si estende a
tutti i metodi, non solo agli accessi ai campi, e anche ad altri prefissi ridondanti, come Fetch, Find e
Lookup.

www.it-ebooks.info
170 CAPITOLO 6. METODI

registro del pacchetto


type Logger struct {
flags int prefix
string
// ...
}
func (l *Logger) Flags() int
func (l *Logger) SetFlags(flag int) func (l
*Logger) Prefix() stringa
func (l *Logger) SetPrefix(prefix string)

Lo stile Go non vieta i campi esportati. Naturalmente, una volta esportato, un campo non può essere
eliminato senza una modifica incompatibile dell'API, quindi la scelta iniziale deve essere deliberata e
deve considerare la complessità degli invarianti che devono essere mantenuti, la probabilità di modifiche
future e la quantità di codice client che verrebbe influenzata da una modifica.
L'incapsulamento non è sempre auspicabile. Rivelando la sua rappresentazione come un numero
int64 di nanosecondi, time.Duration ci consente di utilizzare tutte le consuete operazioni aritmetiche
e di confronto con le durate e persino di definire costanti di questo tipo:
const day = 24 * time.Hour fmt.Println(day.Seconds())
// "86400"

Come altro esempio, confrontiamo IntSet con il tipo geometry.Path dell'inizio di questo capitolo.
Path è stato definito come tipo slice, consentendo ai suoi clienti di costruire istanze utilizzando la
sintassi letterale dello slice, di iterare sui suoi punti utilizzando un ciclo di range e così via, mentre queste
operazioni sono negate ai clienti di IntSet.
Ecco la differenza cruciale: geometry.Path è intrinsecamente una sequenza di punti, né più né meno,
e non prevediamo di aggiungervi nuovi campi, quindi ha senso che il pacchetto geometria riveli che
Path è una slice. Al contrario, un IntSet è semplicemente rappresentato come una slice []uint64.
Avrebbe potuto essere rappresentato con []uint, o con qualcosa di completamente diverso per gli
insiemi radi o molto piccoli, e potrebbe forse beneficiare di caratteristiche aggiuntive come un campo
supplementare per registrare il numero di elementi dell'insieme. Per queste ragioni, ha senso che
IntSet sia opaco.

In questo capitolo abbiamo imparato come associare i metodi ai tipi nominati e come chiamare tali
metodi. Sebbene i metodi siano fondamentali per la programmazione orientata agli oggetti, sono solo
metà del quadro. Per completarlo, abbiamo bisogno delle interfacce, oggetto del prossimo capitolo.

www.it-ebooks.info
7
Interfacce

I tipi di interfaccia esprimono generalizzazioni o astrazioni sul comportamento di altri tipi.


Generalizzando, le interfacce ci permettono di scrivere funzioni più flessibili e adattabili, perché non
sono legate ai dettagli di una particolare implementazione.
Molti linguaggi orientati agli oggetti hanno una qualche nozione di interfacce, ma ciò che rende le
interfacce di Go così particolari è che sono soddisfatte implicitamente. In altre parole, non è necessario
dichiarare tutte le interfacce che un dato tipo concreto soddisfa; è sufficiente possedere i metodi
necessari. Questo design consente di creare nuove interfacce soddisfatte da tipi concreti esistenti senza
modificare i tipi esistenti, il che è particolarmente utile per i tipi definiti in pacchetti che non si
controllano.
In questo capitolo inizieremo ad analizzare le meccaniche di base dei tipi di interfaccia e dei loro valori.
Lungo il percorso, studieremo alcune importanti interfacce della libreria standard. Molti programmi Go
utilizzano le interfacce standard tanto quanto quelle proprie. Infine, esamineremo le asserzioni di tipo
(§7.10) e i commutatori di tipo (§7.13) e vedremo come questi consentano un diverso tipo di generalità.

7.1. Interfacce come contratti

Tutti i tipi che abbiamo visto finora sono tipi concreti. Un tipo concreto specifica l'esatta
rappresentazione dei suoi valori ed espone le operazioni intrinseche di tale rappresentazione, come
l'aritmetica per i numeri o l'indicizzazione, l'append e il range per le fette. Un tipo concreto può anche
fornire comportamenti aggiuntivi attraverso i suoi metodi. Quando si ha un valore di un tipo concreto,
si sa esattamente che cos'è e che cosa si può fare con esso.
Esiste un altro tipo di tipo in Go, chiamato tipo di interfaccia. Un'interfaccia è un tipo astratto. Non
espone la rappresentazione o la struttura interna dei suoi valori, né l'insieme delle funzioni di base di
un'interfaccia.

171

www.it-ebooks.info
172 CAPITOLO 7. INTERFACCE

operazioni che supportano; rivela solo alcuni dei loro metodi. Quando si ha un valore di un tipo di
interfaccia, non si sa nulla di ciò che è; si sa solo cosa può fare, o più precisamente, quali comportamenti
sono forniti dai suoi metodi.
Nel corso del libro abbiamo utilizzato due funzioni simili per la formattazione delle stringhe: fmt.Printf,
che scrive il risultato sullo standard output (un file), e fmt.Sprintf, che restituisce il risultato come
stringa. Sarebbe un peccato se la parte difficile, la formattazione del risultato, dovesse essere duplicata a
causa di queste differenze superficiali nell'utilizzo del risultato. Grazie alle interfacce, non è così.
Entrambe le funzioni sono, in effetti, dei wrapper attorno a una terza funzione, fmt.Fprintf, che è
agnostica rispetto a ciò che accade al risultato che calcola:
pacchetto fmt

func Fprintf(w io.Writer, format string, args ...interface{}) (int, error) func

Printf(format string, args ...interface{}) (int, error) {


return Fprintf(os.Stdout, format, args...)
}
func Sprintf(format string, args ...interface{}) string { var buf
bytes.Buffer
Fprintf(&buf, format, args...) return
buf.String()
}

Il prefisso F di Fprintf sta per file e indica che l'output formattato deve essere scritto nel file fornito
come primo argomento. Nel caso di Printf, l'argomento, os.Std- out, è un *os.File. Nel caso di
Sprintf, invece, l'argomento non è un file, anche se in apparenza ci assomiglia: &buf è un puntatore a
un buffer di memoria in cui possono essere scritti i byte.
Anche il primo parametro di Fprintf non è un file. È un io.Writer, un tipo di interfaccia con la
seguente dichiarazione:
pacchetto io
// Writer è l'interfaccia che avvolge il metodo di base Write. type Writer
interface {
// Write scrive len(p) byte da p al flusso di dati sottostante.
// Restituisce il numero di byte scritti da p (0 <= n <= len(p))
// e qualsiasi errore riscontrato che abbia causato l'interruzione anticipata della scrittura.
// Write deve restituire un errore non nullo se restituisce n < len(p).
// La scrittura non deve modificare i dati della slice, nemmeno temporaneamente.
//
// Le implementazioni non devono conservare p.
Write(p []byte) (n int, err error)
}

L'interfaccia io.Writer definisce il contratto tra Fprintf e i suoi chiamanti. Da un lato, il contratto
richiede che il chiamante fornisca un valore di un tipo concreto come *os.File o
*bytes.Buffer che ha un metodo chiamato Write con la firma e il comportamento appropriati.
D'altra parte, il contratto garantisce che Fprintf farà il suo lavoro con qualsiasi valore che soddisfi
l'interfaccia io.Writer. Fprintf non può presumere che stia scrivendo su un file o su

www.it-ebooks.info
SEZIONE 7.1. INTERFACCE COME CONTRATTI 173

memoria, solo che può chiamare Write.


Poiché fmt.Fprintf non assume nulla sulla rappresentazione del valore e si affida solo ai comportamenti
garantiti dal contratto io.Writer, possiamo tranquillamente passare un valore di qualsiasi tipo con- creto
che soddisfi io.Writer come primo argomento di fmt.Fprintf. Questa libertà di sostituire un tipo con un
altro che soddisfa la stessa interfaccia è chiamata sostituibilità ed è un segno distintivo della
programmazione orientata agli oggetti.
Verifichiamo questo aspetto utilizzando un nuovo tipo. Il metodo Write del tipo *ByteCounter qui
sotto si limita a contare i byte scritti su di esso prima di scartarli. (La conversione è necessaria per far
coincidere i tipi len(p) e *c nell'istruzione di assegnazione +=).
gopl.io/ch7/bytecounter
tipo ByteCounter int
func (c *ByteCounter) Write(p []byte) (int, error) {
*c += ByteCounter(len(p)) // convertire int in ByteCounter return
len(p), nil
}

Poiché *ByteCounter soddisfa il contratto io.Writer, possiamo passarlo a Fprintf, che esegue la
formattazione delle stringhe ignorando questo cambiamento; il ByteCounter accumula
correttamente la lunghezza del risultato.
var c ByteCounter c.Write([]byte("ciao"))
fmt.Println(c) // "5", = len("ciao")
c = 0 // azzeramento del
contatore var name = "Dolly"
fmt.Fprintf(&c, "hello, %s", name) fmt.Println(c) //
"12", = len("hello, Dolly")

Oltre a io.Writer, esiste un'altra interfaccia di grande importanza per il pacchetto fmt. Fprintf e
Fprintln forniscono un modo per i tipi di controllare come vengono stampati i loro valori. Nella
Sezione 2.5, abbiamo definito un metodo String per il tipo Celsius, in modo che le temperature
venissero stampate come "100°C", e nella Sezione 6.5 abbiamo dotato *IntSet di un metodo String,
in modo che gli insiemi venissero resi con la notazione tradizionale degli insiemi, come "{1 2 3}".
Dichiarare un metodo String fa sì che un tipo soddisfi una delle interfacce più utilizzate in assoluto,
fmt.Stringer:
pacchetto fmt
// Il metodo String viene utilizzato per stampare i valori passati
// come operando per qualsiasi formato che accetti una stringa
// o ad una stampante non formattata come Print. type
Stringer interface {
Stringa() stringa
}

Spiegheremo come il pacchetto fmt scopre quali valori soddisfano questa interfaccia nella Sezione 7.10.
Esercizio 7.1: Utilizzando le idee di ByteCounter, implementate i contatori per le parole e per le righe.
Troverete utile bufio.ScanWords.

www.it-ebooks.info
174 CAPITOLO 7. INTERFACCE

Esercizio 7.2: Scrivere una funzione CountingWriter con la firma sottostante che, dato un
io.Writer, restituisca un nuovo Writer che avvolge l'originale e un puntatore a una variabile int64
che in qualsiasi momento contiene il numero di byte scritti nel nuovo Writer.
func CountingWriter(w io.Writer) (io.Writer, *int64)

Esercizio 7.3: Scrivere un metodo String per il tipo *albero in gopl.io/ch4/treesort (§4.4) che
riveli la sequenza di valori nell'albero.

7.2. Interfaccia Tipi di interfaccia

Un tipo di interfaccia specifica un insieme di metodi che un tipo concreto deve possedere per essere
considerato un'istanza di quell'interfaccia.

Il tipo io.Writer è una delle interfacce più utilizzate perché fornisce un'astrazione di tutti i tipi su cui è
possibile scrivere byte, tra cui file, buffer di memoria, connessioni di rete, client HTTP, archiviatori,
hasher e così via. Il pacchetto io definisce molte altre interfacce utili. Un Reader rappresenta qualsiasi tipo
da cui è possibile leggere byte, mentre un Closer è qualsiasi valore che si può chiudere, come un file o una
connessione di rete. (A questo punto avrete probabilmente notato la convenzione di denominazione di
molte interfacce a singolo metodo di Go).
pacchetto io

tipo Interfaccia lettore {


Read(p []byte) (n int, err error)
}

tipo Interfaccia Closer {


Close() errore
}

Guardando più lontano, troviamo dichiarazioni di nuovi tipi di interfaccia come combinazioni di quelli
esistenti. Ecco due esempi:
tipo ReadWriter interface { Reader
Scrittore
}

tipo ReadWriteCloser interface {


Reader
Scrittor
e più
vicino
}

La sintassi usata sopra, che assomiglia all'incorporazione di una struct, ci consente di nominare un'altra
interfaccia come abbreviazione per scrivere tutti i suoi metodi. Questa operazione si chiama
incorporazione di un'interfaccia. Avremmo potuto scrivere io.ReadWriter senza incorporazione, anche
se in modo meno sintetico, in questo modo:

www.it-ebooks.info
SEZIONE 7.3. SODDISFAZIONE 175
DELL'INTERFACCIA

tipo Interfaccia ReadWriter {


Read(p []byte) (n int, err error) Write(p
[]byte) (n int, err error)
}

o addirittura utilizzando un mix dei due stili:


tipo Interfaccia ReadWriter {
Read(p []byte) (n int, err error)
Scrittore
}

Tutte e tre le dichiarazioni hanno lo stesso effetto. L'ordine in cui compaiono i metodi è irrilevante.
L'unica cosa che conta è l'insieme dei metodi.
Esercizio 7.4: La funzione strings.NewReader restituisce un valore che soddisfa l'interfaccia io.Reader
(e altre) leggendo dal suo argomento, una stringa. Implementate voi stessi una semplice versione di
NewReader e usatela per far sì che il parser HTML (§5.2) prenda input da una stringa.

Esercizio 7.5: La funzione LimitReader del pacchetto io accetta un io.Reader r e un numero di byte n, e
restituisce un altro Reader che legge da r ma segnala una condizione di fine file dopo n byte.
Implementatela.
func LimitReader(r io.Reader, n int64) io.Reader

7.3. Interfaccia Soddisfazione

Un tipo soddisfa un'interfaccia se possiede tutti i metodi richiesti dall'interfaccia. Ad esempio, un


*os.File soddisfa io.Reader, Writer, Closer e ReadWriter. Un *bytes.Buffer soddisfa Reader, Writer
e ReadWriter, ma non soddisfa Closer perché non ha un metodo Close. Come abbreviazione, i
programmatori di Go spesso dicono che un tipo concreto ''è un'' particolare tipo di interfaccia, il che
significa che soddisfa l'interfaccia. Ad esempio, un *bytes.Buffer è un io.Writer; un *os.File è un
io.ReadWriter.

La regola di assegnabilità (§2.4.2) per le interfacce è molto semplice: un'espressione può essere assegnata
a un'interfaccia solo se il suo tipo soddisfa l'interfaccia. Quindi:
var w io.Writer
w = os.Stdout // OK: *os.File ha il metodo Write
w = new(bytes.Buffer) // OK: *bytes.Buffer ha il metodo Write
w = time.Second // errore di compilazione: time.Duration manca del metodo Write

var rwc io.ReadWriteCloser


rwc = os.Stdout // OK: *os.File ha metodi di lettura, scrittura e chiusura
rwc = new(bytes.Buffer) // errore di compilazione: *bytes.Buffer non ha il metodo Close

Questa regola si applica anche quando il lato destro è esso stesso un'interfaccia:
w = rwc // OK: io.ReadWriteCloser ha il metodo di scrittura
rwc = w // errore di compilazione: il metodo io.Writer manca di Close

www.it-ebooks.info
176 CAPITOLO 7. INTERFACCE

Poiché ReadWriter e ReadWriteCloser includono tutti i metodi di Writer, qualsiasi tipo che soddisfa
ReadWriter o ReadWriteCloser soddisfa necessariamente Writer.
Prima di proseguire, occorre spiegare una sottigliezza nel significato di metodo p e r un tipo. Ricordiamo
dalla Sezione 6.2 che per ogni tipo concreto T, alcuni dei suoi metodi hanno un destinatario del tipo T
stesso, mentre altri richiedono un puntatore *T. Ricordiamo anche che è legale chiamare un metodo *T su
un argomento di tipo T, purché l'argomento sia una variabile; il compilatore ne prende implicitamente
l'indirizzo. Ma questo è un mero zucchero sintattico: un valore di tipo T non possiede tutti i metodi che
possiede un puntatore *T, e di conseguenza potrebbe soddisfare un numero minore di interfacce.
Un esempio chiarirà questo punto. Il metodo String del tipo IntSet della Sezione 6.5 richiede un
puntatore ricevitore, quindi non si può chiamare questo metodo su un valore IntSet non indirizzabile:
type IntSet struct { /* ... */ } func
(*IntSet) String() string
var _ = IntSet{}.String() // errore di compilazione: String richiede il ricevitore *IntSet

ma possiamo chiamarlo su una variabile IntSet:


var s IntSet
var _ = s.String() // OK: s è una variabile e &s ha un metodo String

Tuttavia, dato che solo *IntSet ha un metodo String, solo *IntSet soddisfa il metodo fmt.Stringer
interfaccia:
var _ fmt.Stringer = &s // OK
var _ fmt.Stringer = s // errore di compilazione: IntSet non ha il metodo String

La sezione 12.8 include un programma che stampa i metodi di un valore arbitrario e lo


strumento godoc -analysis=type (§10.7.4) visualizza i metodi di ogni tipo e la relazione tra interfacce e tipi
concreti.
Come una busta che avvolge e nasconde la lettera che contiene, un'interfaccia avvolge e nasconde il tipo
concreto e il valore che contiene. Solo i metodi rivelati dal tipo di interfaccia possono essere chiamati,
anche se il tipo concreto ne ha altri:
os.Stdout.Write([]byte("hello")) // OK: *os.File ha il metodo Write
os.Stdout.Close() // OK: *os.File ha il metodo Close
var w io.Writer w
= os.Stdout
w.Write([]byte("ciao")) // OK: io.Writer ha il metodo Write
w.Close() // errore di compilazione: il metodo io.Writer manca di Close

Un'interfaccia con più metodi, come io.ReadWriter, ci dice di più sui valori che contiene e pone
maggiori requisiti ai tipi che la implementano, rispetto a un'interfaccia con meno metodi, come
io.Reader. Quindi, cosa ci dice il tipo interface{}, che non ha alcun metodo, sui tipi concreti che lo
soddisfano?
Esatto: niente. Può sembrare inutile, ma in realtà il tipo interface{}, chiamato tipo di interfaccia vuota, è
indispensabile. Poiché il tipo interfaccia vuota non pone alcuna richiesta ai tipi che la soddisfano,
possiamo assegnare qualsiasi valore all'interfaccia vuota.

www.it-ebooks.info
SEZIONE 7.3. SODDISFAZIONE 177
DELL'INTERFACCIA

var any interface{} any =


true
qualsiasi = 12.34
qualsiasi =
"ciao"
any = map[string]int{"one": 1} any =
new(bytes.Buffer)

Anche se non era ovvio, abbiamo usato il tipo di interfaccia vuoto fin dal primo esempio di questo libro,
perché è quello che permette a funzioni come fmt.Println, o errorf nella Sezione 5.7, di accettare
argomenti di qualsiasi tipo.

Naturalmente, dopo aver creato un valore interface{} contenente un booleano, un float, una stringa,
una mappa, un puntatore o qualsiasi altro tipo, non possiamo fare nulla direttamente sul valore che
contiene, poiché l'interfaccia non ha metodi. Abbiamo bisogno di un modo per recuperare il valore.
Vedremo come farlo utilizzando un'asserzione di tipo nella Sezione 7.10.

Poiché la soddisfazione delle interfacce dipende solo dai metodi dei due tipi coinvolti, non è necessario
dichiarare la relazione tra un tipo concreto e le interfacce che soddisfa. Detto questo, di tanto in tanto è
utile documentare e dichiarare la relazione quando è prevista ma non è applicata dal programma. La
dichiarazione seguente afferma in fase di compilazione che un valore di tipo *bytes.Buffer soddisfa
io.Writer:

// *bytes.Buffer deve soddisfare io.Writer var w


io.Writer = new(bytes.Buffer)

Non è necessario allocare una nuova variabile, poiché qualsiasi valore di tipo *bytes.Buffer va bene,
anche nil, che scriviamo come (*bytes.Buffer)(nil) usando una conversione esplicita. E poiché non
intendiamo mai fare riferimento a w, possiamo sostituirlo con l'identificatore vuoto. Insieme, queste
modifiche ci danno questa variante più frugale:
// *bytes.Buffer deve soddisfare io.Writer var
_ io.Writer = (*bytes.Buffer)(nil)

I tipi di interfaccia non vuoti, come io.Writer, sono più spesso soddisfatti da un tipo di puntatore, in
particolare quando uno o più metodi dell'interfaccia implicano un qualche tipo di mutazione per il
destinatario, come il metodo Write. Un puntatore a una struct è un tipo di supporto al metodo
particolarmente comune.

Ma i tipi di puntatore non sono affatto gli unici tipi che soddisfano le interfacce, e anche le interfacce
con metodi mutatori possono essere soddisfatte da uno degli altri tipi di riferimento di Go. Abbiamo
visto esempi di tipi slice con metodi (geometry.Path, §6.1) e di tipi map con metodi (url.Values,
§6.2.1), e più avanti vedremo un tipo function con metodi (http.HandlerFunc,
§7.7). Anche i tipi di base possono soddisfare le interfacce; come abbiamo visto nella Sezione 7.4,
time.Duration soddisfa fmt.Stringer.

Un tipo concreto può soddisfare molte interfacce non correlate. Si consideri un programma che
organizza o vende artefatti culturali digitalizzati come musica, film e libri. Potrebbe definire il seguente
insieme di tipi concreti:

www.it-ebooks.info
178 CAPITOLO 7. INTERFACCE

Album Libro
Film Rivista
Podcast
TVEpisode
Traccia

Possiamo esprimere ogni astrazione di interesse come un'interfaccia. Alcune proprietà sono comuni a tutti gli
artefatti, come il titolo, la data di creazione e l'elenco dei creatori (autori o artisti).
type Artifact interface {
Title() string Creators()
[]string Created()
time.Time
}

Altre proprietà sono limitate a determinati tipi di artefatti. Le proprietà della parola stampata sono
rilevanti solo per i libri e le riviste, mentre solo i film e gli episodi televisivi hanno una risoluzione dello
schermo.
type Text interface {
Pagine() int
Parole() int
PageSize() int
}
tipo Interfaccia audio {
Stream() (io.ReadCloser, errore) RunningTime()
time.Duration
Format() stringa // ad esempio, "MP3", "WAV".
}
tipo Interfaccia video {
Stream() (io.ReadCloser, errore) RunningTime()
time.Duration
Format() stringa // ad esempio, "MP4", "WMV"
Risoluzione() (x, y int)
}

Queste interfacce sono solo un modo utile per raggruppare tipi concreti correlati ed esprimere le
sfaccettature che hanno in comune. Potremmo scoprire altri raggruppamenti in seguito. Per esempio, se
ci accorgiamo di dover gestire elementi Audio e Video nello stesso modo, possiamo definire un'interfaccia
Streamer per rappresentare i loro aspetti comuni senza modificare le dichiarazioni dei tipi esistenti.
tipo Interfaccia Streamer {
Stream() (io.ReadCloser, error)
RunningTime() time.Duration Format()
string
}

Ogni raggruppamento di tipi concreti basato sui loro comportamenti condivisi può essere espresso come
un tipo interattivo. A differenza dei linguaggi basati su classi, in cui l'insieme delle interfacce soddisfatte
da una classe è

www.it-ebooks.info
SEZIONE 7.4. PARSING DEI FLAG CON FLAG.VALUE 179

esplicito, in Go possiamo definire nuove astrazioni o raggruppamenti di interesse quando ne abbiamo


bisogno, senza modificare la dichiarazione del tipo concreto. Questo è particolarmente utile quando il
tipo concreto proviene da un pacchetto scritto da un autore diverso. Naturalmente, è necessario che i tipi
concreti abbiano dei punti in comune.

7.4. Parsing dei flag con flag.Value

In questa sezione vedremo come un'altra interfaccia standard, flag.Value, ci aiuti a definire nuove
notazioni per i flag della riga di comando. Consideriamo il programma seguente, che dorme per un
determinato periodo di tempo.
gopl.io/ch7/sleep
var period = flag.Duration("period", 1*time.Second, "sleep period")

func main() {
flag.Parse()
fmt.Printf("Dormire per %v...", *periodo)
time.Sleep(*periodo)
fmt.Println()
}

Prima di andare a dormire, stampa il periodo di tempo. Il pacchetto fmt richiama il metodo
String di time.Duration per stampare il periodo non come numero di nanosecondi, ma in una notazione
facile da usare:
$ go build gopl.io/ch7/sleep
$ ./sleep
Dormire per 1s...

Per impostazione predefinita, il periodo di sospensione è di un secondo, ma può essere controllato


tramite il flag -periodo com- mandato. La funzione flag.Duration crea una variabile flag di tipo
time.Duration e consente all'utente di specificare la durata in una varietà di formati facili da usare,
compresa la stessa notazione stampata dal metodo String. Questa simmetria di progettazione porta a
un'interfaccia utente piacevole.
$ ./sleep -periodo 50ms
Dorme per 50ms...
$ ./sleep -periodo 2m30s Dorme per
2m30s...
$ ./sleep -periodo 1,5h Dorme per
1h30m0s...
$ ./sleep -periodo "1 giorno"
valore non valido "1 giorno" per il flag -periodo: tempo: durata non valida 1 giorno

Poiché i flag con valore di durata sono molto utili, questa funzione è stata integrata nel pacchetto flag,
ma è facile definire nuove notazioni di flag per i nostri tipi di dati. È sufficiente definire un tipo che
soddisfi l'interfaccia flag.Value, la cui dichiarazione è riportata di seguito:

www.it-ebooks.info
180 CAPITOLO 7. INTERFACCE

bandiera del pacchetto

// Value è l'interfaccia per il valore memorizzato in un flag. type


Value interface {
String() stringa
Set(stringa) errore
}

Il metodo String formatta il valore del flag per utilizzarlo nei messaggi di aiuto della riga di
comando; pertanto ogni flag.Value è anche un fmt.Stringer. Il metodo Set analizza il suo
argomento stringa e aggiorna il valore del flag. In effetti, il metodo Set è l'inverso del metodo String ed
è buona norma che usino la stessa notazione.

Definiamo un tipo celsiusFlag che permetta di specificare una temperatura in Celsius o in


Fahrenheit con una conversione appropriata. Si noti che celsiusFlag incorpora un Celsius (§2.5),
ottenendo così gratuitamente un metodo String. Per soddisfare flag.Value, è sufficiente dichiarare il
metodo Set:

gopl.io/ch7/tempconv
// *celsiusFlag soddisfa l'interfaccia flag.Value. type
celsiusFlag struct{ Celsius }

func (f *celsiusFlag) Set(s string) error { var unit


string
var valore float64
fmt.Sscanf(s, "%f%s", &value, &unit) // non è necessario un controllo degli errori
switch unit {
caso "C", "°C":
f.Celsius = Celsius(valore) return
nil
caso "F", "°F":
f.Celsius = FToC(Fahrenheit(valore))
return nil
}
return fmt.Errorf("temperatura non valida %q", s)
}

La chiamata a fmt.Sscanf analizza un numero in virgola mobile (valore) e una stringa (unità)
dall'ingresso s. Sebbene di solito si debba controllare il risultato dell'errore di Sscanf, in questo caso non
ce n'è bisogno perché se c'è stato un problema, nessun caso di switch corrisponderà.

La funzione CelsiusFlag, descritta di seguito, chiude il tutto. Al chiamante restituisce un puntatore al


campo Celsius incorporato nella variabile celsiusFlag f. Il campo Celsius è la variabile che verrà
aggiornata dal metodo Set durante l'elaborazione dei flag. La chiamata a Var aggiunge il flag
all'insieme dei flag della riga di comando dell'applicazione, la variabile globale flag.CommandLine. I
programmi con interfacce a riga di comando insolitamente complesse possono avere diverse variabili di
questo tipo. La chiamata a Var assegna un argomento *celsiusFlag a un parametro flag.Value,
facendo sì che il compilatore controlli che *celsiusFlag abbia i metodi necessari.

www.it-ebooks.info
SEZIONE 7.5. VALORI 181
DELL'INTERFACCIA

// CelsiusFlag definisce un flag Celsius con il nome specificato,


// valore predefinito e utilizzo e restituisce l'indirizzo della variabile flag.
// L'argomento flag deve avere una quantità e un'unità, ad e s e m p i o "100C".
func CelsiusFlag(name string, value Celsius, usage string) *Celsius {
f := celsiusFlag{value} flag.CommandLine.Var(&f,
name, usage) return &f.Celsius
}

Ora possiamo iniziare a usare il nuovo flag nei nostri programmi:


gopl.io/ch7/tempflag
var temp = tempconv.CelsiusFlag("temp", 20.0, "la temperatura")
func main() {
flag.Parse() fmt.Println(*temp)
}

Ecco una sessione tipica:


$ go build gopl.io/ch7/tempflag
$ ./tempflag 20°C
$ ./tempflag -temp -18C
-18°C
$ ./tempflag -temp 212°F
100°C
$ ./tempflag -temp 273,15K
valore non valido "273,15K" per il flag -temp: temperatura non valida "273,15K" Uso
di ./tempflag:
-valore di temperatura
la temperatura (predefinita 20°C)
$ ./tempflag -help Uso
di ./tempflag:
-valore di temperatura
la temperatura (predefinita 20°C)

Esercizio 7.6: Aggiungere il supporto per le temperature Kelvin a tempflag.


Esercizio 7.7: Spiegare perché il messaggio di aiuto contiene °C mentre il valore predefinito di 20,0
non lo contiene.

7.5. Interfaccia Valori

Concettualmente, un valore di un tipo di interfaccia, o valore di interfaccia, ha due componenti, un tipo


concreto e un valore di quel tipo. Questi sono chiamati tipo dinamico e valore dinamico dell'interfaccia.
Per un linguaggio tipizzato staticamente come Go, i tipi sono un concetto a tempo di compilazione, quindi
un tipo non è un valore. Nel nostro modello concettuale, un insieme di valori chiamati descrittori di tipo
forniscono informazioni

www.it-ebooks.info
182 CAPITOLO 7. INTERFACCE

di ogni tipo, come il nome e i metodi. In un valore di interfaccia, il componente del tipo è rappresentato
dal descrittore di tipo appropriato.
Nelle quattro affermazioni seguenti, la variabile w assume tre valori diversi. (I valori iniziali e finali sono
gli stessi).
var w io.Writer w
= os.Stdout
w = new(bytes.Buffer) w
= nil

Osserviamo più da vicino il valore e il comportamento dinamico di w dopo ogni dichiarazione. La prima
dichiarazione dichiara w:
var w io.Writer

In Go, le variabili sono sempre inizializzate a un valore ben definito e le interfacce non fanno eccezione.
Il valore zero di un'interfaccia ha entrambi i componenti tipo e valore impostati su nil (Figura 7.1).

Figura 7.1. Un valore di interfaccia nullo.


Un valore di interfaccia è descritto come nil o non-nil in base al suo tipo dinamico, quindi questo è un
valore di interfaccia nil. È possibile verificare se un valore di interfaccia è nil utilizzando w == nil o w !=
nil. La chiamata di un metodo di un valore di interfaccia nil provoca un panico:
w.Write([]byte("hello")) // panico: dereferenziazione del puntatore nil

La seconda istruzione assegna a w un valore di tipo *os.File:


w = os.Stdout

Questo assegnamento comporta una conversione implicita da un tipo concreto a un tipo di interfaccia
ed è equivalente alla conversione esplicita io.Writer(os.Stdout). Una conversione di questo tipo,
esplicita o implicita, cattura il tipo e il valore del suo operando. Il tipo dinamico del valore
dell'interfaccia è impostato sul descrittore di tipo per il tipo di puntatore *os.File e il suo valore
dinamico contiene una copia di os.Stdout, che è un puntatore alla variabile os.File che rappresenta
l'output standard del processo (Figura 7.2).

Figura 7.2. Un valore di interfaccia contenente un puntatore *os.File.

www.it-ebooks.info
SEZIONE 7.5. VALORI 183
DELL'INTERFACCIA

La chiamata del metodo Write su un valore dell'interfaccia contenente un puntatore *os.File provoca
l'apertura del file
(*os.File).Write da chiamare. La chiamata stampa "ciao".
w.Write([]byte("hello")) // "ciao"

In generale, non è possibile sapere in fase di compilazione quale sarà il tipo dinamico di un valore di
interfaccia, quindi una chiamata attraverso un'interfaccia deve utilizzare il dispatch dinamico. Invece di
una chiamata diretta, il compilatore deve generare codice per ottenere l'indirizzo del metodo
chiamato Write dal descrittore di tipo, quindi effettuare una chiamata indiretta a tale indirizzo.
L'argomento del destinatario della chiamata è una copia del valore dinamico dell'interfaccia,
os.Stdout. L'effetto è quello di una chiamata diretta:
os.Stdout.Write([]byte("hello")) // "ciao"

La terza istruzione assegna un valore di tipo *bytes.Buffer al valore dell'interfaccia:


w = new(bytes.Buffer)

Il tipo dinamico è ora *bytes.Buffer e il valore dinamico è un puntatore al nuovo buffer allocato
(Figura 7.3).

Figura 7.3. Un valore di interfaccia contenente un puntatore *bytes.Buffer.


Una chiamata al metodo Write utilizza lo stesso meccanismo di prima:
w.Write([]byte("ciao")) // scrive "ciao" nel byte.Buffer

Questa volta, il descrittore di tipo è *bytes.Buffer, quindi viene chiamato il metodo


(*bytes.Buffer).Write, con l'indirizzo del buffer come valore del parametro receiver. La chiamata
aggiunge "hello" al buffer.
Infine, la quarta istruzione assegna nil al valore dell'interfaccia:
w = nil

Questo resetta entrambi i suoi componenti a nil, riportando w allo stesso stato in cui era stato dichiarato,
come mostrato nella Figura 7.1.
Un valore di interfaccia può contenere valori dinamici di dimensioni arbitrarie. Ad esempio, il tipo
time.Time, che rappresenta un istante nel tempo, è un tipo struct con diversi campi non esportati. Se
creiamo un valore di interfaccia da questo tipo,
var x interface{} = time.Now()

il risultato potrebbe essere simile a quello della Figura 7.4. Concettualmente, il valore dinamico si
inserisce sempre all'interno del valore dell'interfaccia, indipendentemente dal suo tipo. (Questo è solo un
modello concettuale; un'implementazione realistica è molto diversa).

www.it-ebooks.info
184 CAPITOLO 7. INTERFACCE

Figura 7.4. Un valore di interfaccia che contiene una struttura time.Time.


I valori di interfaccia possono essere confrontati utilizzando == e !=. Due valori di interfaccia sono uguali
se entrambi sono nulli, oppure se i loro tipi dinamici sono identici e i loro valori dinamici sono uguali
secondo il comportamento abituale di == per quel tipo. Poiché i valori di interfaccia sono confrontabili,
possono essere utilizzati come chiavi di una mappa o come operando di un'istruzione switch.
Tuttavia, se vengono confrontati due valori di interfaccia che hanno lo stesso tipo dinamico, ma questo
tipo non è comparabile (ad esempio una slice), il confronto fallisce con un panico:
var x interface{} = []int{1, 2, 3}
fmt.Println(x == x) // panico: confronto tra tipi non confrontabili []int

Da questo punto di vista, i tipi di interfaccia sono insoliti. Altri tipi sono confrontabili in modo sicuro
(come i tipi base e i puntatori) o non lo sono affatto (come le slices, le mappe e le funzioni), ma quando
si confrontano i valori delle interfacce o i tipi aggregati che contengono valori delle interfacce, bisogna
essere consapevoli del potenziale di panico. Un rischio simile esiste quando si usano le interfacce come
chiavi di mappa o operandi di switch. Confrontate i valori delle interfacce solo se siete certi che
contengono valori dinamici di tipo comparabile.
Quando si gestiscono gli errori o durante il debug, è spesso utile riportare il tipo dinamico di un valore
dell'interfaccia. A tale scopo, si utilizza il verbo %T del pacchetto fmt:
var w io.Writer fmt.Printf("%T\n", w)
// "<nil>"

w = os.Stdout
fmt.Printf("%T\n", w) // "*os.File"

w = new(bytes.Buffer)
fmt.Printf("%T\n", w) // "*bytes.Buffer"

Internamente, fmt utilizza la riflessione per ottenere il nome del tipo dinamico dell'interfaccia. Vedremo
la riflessione nel Capitolo 12.

7.5.1. Avvertenza: un'interfaccia che contiene un puntatore nullo è non nullo

Un valore di interfaccia nil, che non contiene alcun valore, non è la stessa cosa di un valore di interfaccia
che contiene un puntatore che si dà il caso sia nil. Questa sottile distinzione crea una trappola in cui ogni
programmatore Go è inciampato.

www.it-ebooks.info
SEZIONE 7.5. VALORI 185
DELL'INTERFACCIA

Si consideri il programma seguente. Con il debug impostato su true, la funzione main raccoglie l'output
della funzione f in un bytes.Buffer.
const debug = true func

main() {
var buf *bytes.Buffer
se debug {
buf = new(bytes.Buffer) // abilita la raccolta dell'output
}
f(buf) // NOTA: sottilmente errato! if
debug {
// ...usare buf...
}
}
// Se out è non-nil, l'output verrà scritto su di esso. func
f(out io.Writer) {
// ...fare qualcosa... if
out != nil {
out.Write([]byte("done!\n"))
}
}

Ci si potrebbe aspettare che la modifica di debug a false disabiliti la raccolta dell'output, ma in realtà
provoca il panico del programma durante la chiamata out.Write:
if out != nil {
out.Write([]byte("done!\n")) // panico: dereferenziazione del puntatore nil
}

Quando main chiama f, assegna un puntatore nil di tipo *bytes.Buffer al parametro out, quindi il
valore dinamico di out è nil. Tuttavia, il suo tipo dinamico è *bytes.Buffer, il che significa che out
è un'interfaccia non nil contenente un puntatore nil (Figura 7.5), quindi il controllo difensivo out
!= nil è ancora vero.

Figura 7.5. Un'interfaccia non nil contenente un puntatore nil.


Come in precedenza, il meccanismo di dispatch dinamico determina che (*bytes.Buffer).Write
deve essere chiamato, ma questa volta con un valore di ricevitore che è nil. Per alcuni tipi, come
*os.File, nil è un ricevitore valido (§6.2.1), ma *bytes.Buffer non è tra questi. Il metodo viene chiamato, ma
va in panico quando cerca di accedere al buffer.
Il problema è che, sebbene un puntatore *bytes.Buffer nil abbia i metodi necessari per soddisfare
l'interfaccia, non soddisfa i requisiti comportamentali dell'interfaccia. In particolare, il metodo

www.it-ebooks.info
186 CAPITOLO 7. INTERFACCE

viola la precondizione implicita di (*bytes.Buffer).Write che il suo destinatario non sia nil, quindi
assegnare il puntatore nil all'interfaccia è stato un errore. La soluzione è cambiare il tipo di buf nel main
in io.Writer, evitando così di assegnare il valore disfunzionale all'interfaccia:
var buf io.Writer if
debug {
buf = new(bytes.Buffer) // abilita la raccolta dell'output
}
f(buf) // OK

Dopo aver trattato la meccanica dei valori delle interfacce, diamo un'occhiata ad alcune interfacce più
importanti della libreria standard di Go. Nelle prossime tre sezioni vedremo come le interfacce vengono
utilizzate per l'ordinamento, il servizio web e la gestione degli errori.

7.6. Ordinamento con sort.Interface

Come la formattazione delle stringhe, l'ordinamento è un'operazione frequentemente utilizzata in molti


programmi. Sebbene un Quicksort minimale possa essere scritto in circa 15 righe, un'implementazione
robusta è molto più lunga e non è il tipo di codice che vorremmo scrivere di nuovo o copiare ogni volta
che ne abbiamo bisogno.

Fortunatamente, il pacchetto sort fornisce un ordinamento in-place di qualsiasi sequenza secondo


qualsiasi funzione di ordinamento. Il suo design è piuttosto insolito. In molti linguaggi, l'algoritmo di
ordinamento è associato al tipo di dati della sequenza, mentre la funzione di ordinamento è associata al
tipo degli elementi. Al contrario, la funzione sort.Sort di Go non presuppone nulla sulla
rappresentazione della sequenza o dei suoi elementi. Utilizza invece un'interfaccia, sort.Interface, per
specificare il contratto tra l'algoritmo di ordinamento generico e ogni tipo di sequenza che può essere
ordinata. Un'implementazione di questa interfaccia determina sia la rappresentazione concreta della
sequenza, che spesso è una slice, sia l'ordinamento desiderato dei suoi elementi.

Un algoritmo di ordinamento in-place ha bisogno di tre cose: la lunghezza della sequenza, un mezzo per
unire due elementi e un modo per scambiare due elementi:
ordinamento del pacchetto

tipo Interfaccia Interfaccia { Len()


int
Less(i, j int) bool // i, j sono indici di elementi della sequenza
Swap(i, j int)
}

Per ordinare qualsiasi sequenza, occorre definire un tipo che implementi questi tre metodi, quindi
applicare sort.Sort a un'istanza di quel tipo. Come esempio forse più semplice, consideriamo
l'ordinamento di una fetta di stringhe. Il nuovo tipo StringSlice e i suoi metodi Len, Less e Swap sono
mostrati di seguito.

www.it-ebooks.info
SEZIONE 7.6. ORDINAMENTO CON SORT.INTERFACE 187

tipo StringSlice []string

func (p StringSlice) Len() int { return len(p) }


func (p StringSlice) Less(i, j int) bool { return p[i] < p[j] } func (p
StringSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }

Ora possiamo ordinare una fetta di stringhe, i nomi, convertendo la fetta in una StringSlice in questo modo:
sort.Sort(StringSlice(names))

La conversione produce un valore di slice con la stessa lunghezza, capacità e array sottostante di
ma con un tipo che abbia i tre metodi necessari per l'ordinamento.
L'ordinamento di una fetta di stringhe è così comune che il pacchetto sort fornisce il tipo
StringSlice e una funzione chiamata Strings, in modo che la chiamata precedente possa
essere semplificata in sort.Strings(nomi).
La tecnica qui descritta è facilmente adattabile ad altri ordini di ordinamento, ad esempio per ignorare la
capitalizzazione o i caratteri speciali. (Il programma Go che ordina i termini dell'indice e i numeri di
pagina per questo libro lo fa, con una logica aggiuntiva per i numeri romani). Per un ordinamento più
complicato, si usa la stessa idea, ma con strutture di dati più complicate o implementazioni più
complicate dei metodi sort.Interface.
Il nostro esempio di ordinamento sarà una playlist musicale, visualizzata come una tabella. Ogni brano è
una singola riga e ogni colonna è un attributo di quel brano, come l'artista, il titolo e il tempo di
esecuzione. Immaginiamo che un'interfaccia grafica presenti la tabella e che facendo clic sull'intestazione
di una colonna la playlist venga ordinata in base a quell'attributo; facendo di nuovo clic sull'intestazione
della stessa colonna l'ordine viene invertito. Vediamo cosa potrebbe accadere in risposta a ciascun clic.
La variabile tracks qui sotto contiene una playlist. (Uno degli autori si scusa per i gusti musicali
dell'altro). Ogni elemento è indiretto, un puntatore a una traccia. Anche se il codice sottostante
funzionerebbe se memorizzassimo direttamente le tracce, la funzione di ordinamento scambierà molte
coppie di elementi, quindi sarà più veloce se ogni elemento è un puntatore, cioè una singola parola
macchina, invece di un'intera traccia, che potrebbe essere di otto parole o più.
gopl.io/ch7/sorting
type Track struct {
Titolo stringa
Artista stringa
Album stringa
Anno int
Lunghezza tempo.Durata
}

var tracce = []*Traccia{


{"Go", "Delilah", "From the Roots Up", 2012, durata("3m38s")},
{"Go", "Moby", "Moby", 1992, length("3m37s")},
{"Go Ahead", "Alicia Keys", "As I Am", 2007, durata("4m36s")},
{"Ready 2 Go", "Martin Solveig", "Smash", 2011, durata("4m24s")},
}

www.it-ebooks.info
188 CAPITOLO 7. INTERFACCE

func length(s string) time.Duration { d, err


:= time.ParseDuration(s) if err := nil
{
panico(i)
}
restituire d
}

La funzione printTracks stampa la playlist come tabella. Una visualizzazione grafica sarebbe più
bella, ma questa piccola routine utilizza il pacchetto text/tabwriter per produrre una tabella le cui colonne
sono ordinatamente allineate e imbottite come mostrato di seguito. Osservate che
*tabwriter.Writer soddisfa io.Writer. Raccoglie ogni dato che gli viene scritto; il suo metodo Flush
formatta l'intera tabella e la scrive su os.Stdout.
func printTracks(tracks []*Track) {
const format = "%v\t%v\t%v\t%v\t%v\t%v\tn"
tw := new(tabwriter.Writer).Init(os.Stdout, 0, 8, 2, ' ', 0) fmt.Fprintf(tw,
format, "Title", "Artist", "Album", "Year", "Length") fmt.Fprintf(tw, format,
"-----", "------", "-----", "----", "----------------------------------------------------------------------------")
per _, t := intervallo di tracce {
fmt.Fprintf(tw, format, t.Title, t.Artist, t .Album, t.Year, t.Length)
}
tw.Flush() // calcola la larghezza delle colonne e stampa la tabella
}

Per ordinare la playlist in base al campo Artista, definiamo un nuovo tipo di slice con i necessari
metodi Len, Less e Swap, analogamente a quanto fatto per StringSlice.
tipo byArtist []*Track

func (x byArtist) Len() int { return len(x) }


func (x byArtist) Less(i, j int) bool { return x[i].Artist < x[j].Artist } func (x
byArtist) Swap(i, j int) { x[i], x[j] = x[j], x[i] }

Per chiamare la routine di ordinamento generico, dobbiamo prima convertire le tracce nel nuovo
tipo, byArtist, che definisce l'ordine:
sort.Sort(byArtist(tracks))

Dopo aver ordinato le slice per artista, l'output di printTracks è


Titolo Artista Album Anno Lunghez
----- ------ ----- ---- za
------
Vai avanti Alicia Keys Come sono 2007 4m36s
Vai Dalila Dalle radici 2012 3m38s
Pronti a partire Martin Solveig Smash 2011 4m24s
Vai Moby Moby 1992 3m37s

Se l'utente richiede ''ordina per artista'' una seconda volta, ordineremo i brani al contrario. Non è
necessario definire un nuovo tipo byReverseArtist con un metodo Less invertito, tuttavia, poiché il
pacchetto sort fornisce una funzione Reverse che trasforma qualsiasi ordine in un ordine inverso.

www.it-ebooks.info
SEZIONE 7.6. ORDINAMENTO CON SORT.INTERFACE 189

sort.Sort(sort.Reverse(byArtist(tracks)))

Dopo l'ordinamento inverso della fetta per artista, l'output di printTracks è


Titolo Artista Album Anno Lunghez
----- ------ ----- ---- za
------
Vai Moby Moby 1992 3m37s
Pronti a partire Martin Solveig Smash 2011 4m24s
Vai Dalila Dalle radici 2012 3m38s
Vai avanti Alicia Keys Come sono 2007 4m36s

La funzione sort.Reverse merita un'analisi più approfondita, poiché utilizza la composizione (§6.3), che è
un'idea importante. Il pacchetto sort definisce un tipo non esportato reverse, che è una struct che
incorpora una sort.Interface. Il metodo Less di reverse richiama il metodo Less del valore
sort.Interface incorporato, ma con gli indici invertiti, invertendo l'ordine dei risultati dell'ordinamento.
ordinamento del pacchetto

tipo reverse struct{ Interface } // cioè sort.Interface

func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) } func


Reverse(data Interface) Interface { return reverse{data} }

Len e Swap, gli altri due metodi di inversione, sono implicitamente forniti dal valore originale di
sort.Interface, poiché si tratta di un campo incorporato. La funzione esportata Reverse
restituisce un'istanza del tipo reverse che contiene il valore sort.Interface originale.
Per ordinare in base a una colonna diversa, occorre definire un nuovo tipo, ad esempio byYear:
tipo perAnno []*Traccia

func (x byYear) Len() int { restituisce len(x) }


func (x byYear) Less(i, j int) bool { return x[i].Year < x[j].Year } func (x
byYear) Swap(i, j int) { x[i], x[j] = x[j], x[i] }

Dopo aver ordinato i brani per anno utilizzando sort.Sort(byYear(tracks)), printTracks mostra
un elenco cronologico:
Titolo Artista Album Anno Lunghez
----- ------ ----- ---- za
------
Vai Moby Moby 1992 3m37s
Vai avanti Alicia Keys Come sono 2007 4m36s
Pronti a partire Martin Solveig Smash 2011 4m24s
Vai Dalila Dalle radici 2012 3m38s

Per ogni tipo di elemento della slice e per ogni funzione di ordinamento di cui abbiamo bisogno,
dichiariamo una nuova implementazione di sort.Interface. Come si può vedere, i metodi Len e Swap
hanno definizioni identiche per tutti i tipi di slice. Nell'esempio successivo, il tipo concreto customSort
combina una slice con una funzione, consentendo di definire un nuovo ordinamento scrivendo solo la
funzione di confronto. Per inciso, i tipi concreti che implementano sort.Interface non sono sempre
slice; customSort è un tipo struct.

www.it-ebooks.info
190 CAPITOLO 7. INTERFACCE

tipo customSort struct { t


[]*Traccia
meno func(x, y *Track) bool
}

func (x customSort) Len() int { restituisce len(x.t) }


func (x customSort) Less(i, j int) bool { return x.less(x.t[i], x.t[j]) } func (x
customSort) Swap(i, j int) { x.t[i], x.t[j] = x.t[j], x.t[i] }

Definiamo una funzione di ordinamento a più livelli la cui chiave primaria di ordinamento è il
titolo, la chiave secondaria è l'anno e la chiave terziaria è il tempo di esecuzione, la lunghezza. Ecco la
chiamata a Sort utilizzando una funzione di ordinamento anonima:
sort.Sort(customSort{tracks, func(x, y *Track) bool { if x.Title
!= y.Title {
restituire x.Titolo < y.Titolo
}
if x.Year != y.Year { return
x.Year < y.Year
}
if x.Length != y.Length { return
x.Length < y.Length
}
restituire false
}})

Ed ecco il risultato. Si noti che il pareggio tra i due brani intitolati ''Go'' è spezzato a favore di quello più
vecchio.
Titolo Artista Album Anno Lunghez
----- ------ ----- ---- za
------
Vai Moby Moby 1992 3m37s
Vai Dalila Dalle radici 2012 3m38s
Vai avanti Alicia Keys Come sono 2007 4m36s
Pronti a partire Martin Solveig Smash 2011 4m24s

Sebbene l'ordinamento di una sequenza di lunghezza n richieda O(n log n) operazioni di confronto,
verificare se una sequenza è già ordinata richiede al massimo n-1 confronti. La funzione IsSorted del
pacchetto sort lo verifica. Come sort.Sort, astrae sia la sequenza che la sua funzione di
ordinamento utilizzando sort.Interface, ma non chiama mai il metodo Swap: Questo codice
dimostra le funzioni IntsAreSorted e Ints e il tipo IntSlice:
valori := []int{3, 1, 4, 1} fmt.Println(sort.IntsAreSorted(valori))
// "falso" sort.Ints(valori)
fmt.Println(valori) // "[1 1 3 4]"
fmt.Println(sort.IntsAreSorted(values)) // "true"
sort.Sort(sort.Reverse(sort.IntSlice(values)) fmt.Println(values)
// "[4 3 1 1]"
fmt.Println(sort.IntsAreSorted(values)) // "false"

www.it-ebooks.info
SEZIONE 7.7. L'INTERFACCIA HTTP.HANDLER 191

Per comodità, il pacchetto sort fornisce versioni delle sue funzioni e dei suoi tipi specializzate per []int,
[]string e []float64 utilizzando i loro ordinamenti naturali. Per altri tipi, come []int64 o []uint,
dobbiamo arrangiarci da soli, anche se il percorso è breve.
Esercizio 7.8: Molte GUI forniscono un widget di tabella con un ordinamento statico a più livelli: la
chiave di ordinamento primaria è la testa di colonna cliccata più di recente, la chiave di ordinamento
secondaria è la seconda testa di colonna cliccata più di recente e così via. Definite un'implementazione di
sort.Interface da utilizzare per una tabella di questo tipo. Confrontate questo approccio con
l'ordinamento ripetuto utilizzando sort.Stable.
Esercizio 7.9: Usare il pacchetto html/template (§4.6) per sostituire printTracks con una funzione che
visualizzi le tracce come tabella HTML. Utilizzate la soluzione dell'esercizio precedente per fare in modo
che ogni clic su una colonna faccia una richiesta HTTP per ordinare la tabella.
Esercizio 7.10: Il tipo sort.Interface può essere adattato ad altri usi. Scrivere una funzione
IsPalindrome(s sort.Interface) bool che indichi se la sequenza s è un palin- dromo, in altre parole
se l'inversione della sequenza non la cambia. Assumiamo che gli elementi agli indici i e j siano uguali se
!s.Less(i, j) && !s.Less(j, i).

7.7. L'interfaccia http.Handler

Nel Capitolo 1 abbiamo visto come utilizzare il pacchetto net/http per implementare client (§1.5) e server
(§1.7). In questa sezione, esamineremo più da vicino l'API del server, il cui fondamento è l'interfaccia
http.Handler:

rete/http
pacchetto http

tipo Interfaccia gestore {


ServeHTTP(w ResponseWriter, r *Request)
}

func ListenAndServe(indirizzo stringa, h Handler) errore

La funzione ListenAndServe richiede un indirizzo del server, ad esempio "localhost:8000", e


un'istanza dell'interfaccia Handler a cui devono essere inviate tutte le richieste. Viene eseguita per
sempre, o finché il server non fallisce (o non si avvia) con un errore, sempre non nullo, che viene
restituito.
Immaginiamo un sito di commercio elettronico con un database che mappa gli articoli in vendita e i loro
prezzi in dollari. Il programma che segue mostra l'implementazione più semplice che si possa
immaginare. Modella l'inven- to come un tipo di mappa, database, a cui abbiamo collegato un
metodo ServeHTTP in modo che soddisfi l'interfaccia http.Handler. L'handler esegue un'analisi della
mappa e stampa gli elementi.
gopl.io/ch7/http1
func main() {
db := database{"scarpe": 50, "calzini": 5} log.Fatal(http.ListenAndServe("localhost:8000",
db))
}

www.it-ebooks.info
192 CAPITOLO 7. INTERFACCE

tipo dollars float32

func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) } type database

map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) { for item, price
:= range db {
fmt.Fprintf(w, "%s: %s\n", articolo, prezzo)
}
}

Se si avvia il server,
$ go build gopl.io/ch7/http1
$ ./http1 &

e connettersi ad esso con il programma fetch della Sezione 1.5 (o con un browser web, se si preferisce), si
ottiene il seguente risultato:
$ go build gopl.io/ch1/fetch
$ ./fetch http://localhost:8000
scarpe: $50.00
calzini: $5,00

Finora, il server può solo elencare l'intero inventario e lo farà per ogni richiesta, indipendentemente
dall'URL. Un server più realistico definisce più URL diversi, ognuno dei quali attiva un
comportamento diverso. Chiamiamo quello esistente /list e aggiungiamone un altro, chiamato
/price, che riporta il prezzo di un singolo articolo, specificato come parametro di richiesta, come
/price?item=socks.

gopl.io/ch7/http2
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) { switch
req.URL.Path {
caso "/list":
per item, prezzo := range db { fmt.Fprintf(w, "%s:
%s\n", item, prezzo)
}
caso "/prezzo":
item := req.URL.Query().Get("item")
price, ok := db[item]
if !ok {
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such item: %q\n", item) return
}
fmt.Fprintf(w, "%s\n", prezzo)
default:
w.WriteHeader(http.StatusNotFound) // 404 fmt.Fprintf(w,
"no such page: %s\n", req.URL)
}
}

www.it-ebooks.info
SEZIONE 7.7. L'INTERFACCIA HTTP.HANDLER 193

Ora il gestore decide quale logica eseguire in base al componente percorso dell'URL, req.URL.Path. Se il
gestore non riconosce il percorso, segnala un errore HTTP al client chiamando
w.WriteHeader(http.StatusNotFound); questo deve essere fatto prima di scrivere qualsiasi testo in
w. (Per inciso, http.ResponseWriter è un'altra interfaccia. Aggiunge a io.Writer i metodi per l'invio
delle intestazioni delle risposte HTTP). In modo equivalente, si potrebbe usare la funzione di
utilità http.Error:
msg := fmt.Sprintf("no such page: %s\n", req.URL) http.Error(w,
msg, http.StatusNotFound) // 404

Il caso di /price chiama il metodo Query dell'URL per analizzare i parametri della richiesta HTTP come
una mappa, o più precisamente una multimappa di tipo url.Values (§6.2.1) dal pacchetto net/url. Trova
quindi il primo parametro dell'articolo e ne cerca il prezzo. Se l'articolo non viene trovato, viene segnalato
un errore.

Ecco un esempio di sessione con il nuovo server:


$ go build gopl.io/ch7/http2
$ go build gopl.io/ch1/fetch
$ ./http2 &
$ ./fetch http://localhost:8000/list scarpe:
$50.00
calzini: $5,00
$ ./fetch http://localhost:8000/price?item=socks
$5.00
$ ./fetch http://localhost:8000/price?item=shoes
$50.00
$ ./fetch http://localhost:8000/price?item=hat nessun
elemento di questo tipo: "cappello"
$ ./fetch http://localhost:8000/help
nessuna pagina di questo tipo: /help

Ovviamente potremmo continuare ad aggiungere casi a ServeHTTP, ma in un'applicazione realistica è


conveniente definire la logica per ogni caso in una funzione o metodo separato. Inoltre, gli URL correlati
possono richiedere una logica simile; diversi file di immagini possono avere URL della forma
/images/*.png, per esempio. Per questi motivi, net/http fornisce ServeMux, un multiplexer di
richieste, per semplificare l'associazione tra URL e gestori. Un ServeMux aggrega un insieme di gestori
http.Handler in un unico gestore http.Handler. Anche in questo caso, vediamo che tipi diversi che
soddisfano la stessa interfaccia sono sostituibili: il server web può inviare le richieste a qualsiasi
http.Handler, indipendentemente dal tipo concreto che si trova dietro di esso.

Per un'applicazione più complessa, possono essere composti diversi ServeMux per gestire requisiti di
dispacciamento più complessi. Go non ha un framework web canonico analogo a Rails di Ruby o Django
di Python. Questo non vuol dire che tali framework non esistano, ma gli elementi della libreria standard
di Go sono abbastanza flessibili da rendere spesso superflui i framework. Inoltre, sebbene i framework
siano comodi nelle prime fasi di un progetto, la loro complessità aggiuntiva può rendere più difficile la
manutenzione a lungo termine.

www.it-ebooks.info
194 CAPITOLO 7. INTERFACCE

Nel programma che segue, creiamo un ServeMux e lo utilizziamo per associare gli URL ai gestori di
risposta per le operazioni /list e /price, che sono stati suddivisi in metodi separati. Utilizziamo quindi il
ServeMux come gestore principale nella chiamata a ListenAndServe.
gopl.io/ch7/http3
func main() {
db := database{"scarpe": 50, "calzini": 5} mux :=
http.NewServeMux()
mux.Handle("/list", http.HandlerFunc(db.list))
mux.Handle("/price", http.HandlerFunc(db.price))
log.Fatal(http.ListenAndServe("localhost:8000", mux))
}

tipo database map[string]dollars

func (db database) list(w http.ResponseWriter, req *http.Request) { for item,


price := range db {
fmt.Fprintf(w, "%s: %s\n", articolo, prezzo)
}
}

func (db database) price(w http.ResponseWriter, req *http.Request) { item :=


req.URL.Query().Get("item")
prezzo, ok := db[item]
if !ok {
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such item: %q\n", item) return
}
fmt.Fprintf(w, "%s\n", prezzo)
}

Concentriamoci sulle due chiamate a mux.Handle che registrano i gestori. Nella prima, db.list
è un valore di metodo (§6.4), cioè un valore di tipo
func(w http.ResponseWriter, req *http.Request)

che, quando viene chiamata, invoca il metodo database.list con il valore del ricevitore db. Quindi
db.list è una funzione che implementa un comportamento simile a un gestore, ma poiché non ha
metodi, non soddisfa l'interfaccia http.Handler e non può essere passata direttamente a
mux.Handle.

L'espressione http.HandlerFunc(db.list) è una conversione, non una chiamata di funzione, in


quanto
http.HandlerFunc è un tipo. Ha la seguente definizione:
rete/http
pacchetto http

tipo HandlerFunc func(w ResponseWriter, r *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r)


}

www.it-ebooks.info
SEZIONE 7.7. L'INTERFACCIA HTTP.HANDLER 195

HandlerFunc dimostra alcune caratteristiche insolite del meccanismo di interfaccia di Go. È un tipo
di funzione che ha metodi e soddisfa un'interfaccia, http.Handler. Il comportamento del suo
metodo ServeHTTP è quello di chiamare la funzione sottostante. HandlerFunc è quindi un adattatore
che consente a un valore di funzione di soddisfare un'interfaccia, dove la funzione e l'unico metodo
dell'interfaccia hanno la stessa firma. In effetti, questo trucco consente a un singolo tipo come il
database di soddisfare l'interfaccia http.Handler in diversi modi: una volta attraverso il suo metodo
list, una volta attraverso il suo metodo price e così via.

Poiché la registrazione di un gestore in questo modo è così comune, ServeMux ha un metodo di


convenienza chiamato HandleFunc che lo fa per noi, quindi possiamo semplificare il codice di
registrazione del gestore a questo:
gopl.io/ch7/http3a
mux.HandleFunc("/list", db.list)
mux.HandleFunc("/price", db.price)

Dal codice sopra riportato è facile capire come si potrebbe costruire un programma in cui ci sono due
server web diversi, in ascolto su porte diverse, che definiscono URL diversi e che si dis-attraggono a
gestori diversi. Basterebbe costruire un altro ServeMux ed effettuare un'altra chiamata a ListenAndServe,
magari in contemporanea. Ma nella maggior parte dei programmi, un server Web è sufficiente. Inoltre, è
tipico definire i gestori HTTP in molti file di un'applicazione e sarebbe una seccatura se tutti dovessero
essere registrati esplicitamente con l'istanza ServeMux dell'applicazione.
Per comodità, net/http fornisce un'istanza globale di ServeMux chiamata DefaultServeMux e funzioni
a livello di pacchetto chiamate http.Handle e http.HandleFunc. Per utilizzare DefaultServeMux come
gestore principale del server, non è necessario passarlo a ListenAndServe; è sufficiente nil.
La funzione principale del server può quindi essere semplificata in
gopl.io/ch7/http4
func main() {
db := database{"scarpe": 50, "calzini": 5}
http.HandleFunc("/list", db.list) http.HandleFunc("/price",
db.price) log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

Infine, un promemoria importante: come abbiamo detto nella Sezione 1.7, il server web invoca ogni
gestore in una nuova goroutine, quindi i gestori devono prendere precauzioni come il blocco quando
accedono a variabili a cui potrebbero accedere altre goroutine, comprese altre richieste allo stesso
gestore. Parleremo di concorrenza nei prossimi due capitoli.
Esercizio 7.11: Aggiungere gestori aggiuntivi in modo che i client possano creare, leggere, aggiornare e
cancellare voci del database. Per esempio, una richiesta del tipo /update?item=socks&price=6 aggiornerà il
prezzo di un articolo nell'inventario e segnalerà un errore se l'articolo non esiste o se il prezzo non è
valido. (Attenzione: questa modifica introduce l'aggiornamento simultaneo delle variabili).
Esercizio 7.12: Modificare il gestore di /list per stampare il suo output come tabella HTML, non
come testo. Può essere utile il pacchetto html/template (§4.6).

www.it-ebooks.info
196 CAPITOLO 7. INTERFACCE

7.8. L'errore Interfaccia

Fin dall'inizio di questo libro, abbiamo usato e creato valori del misterioso tipo di errore
predichiarato senza spiegare cosa sia realmente. In effetti, si tratta solo di un tipo di interfaccia con un
singolo metodo che restituisce un messaggio di errore:

tipo error interface {


Error() string
}

Il modo più semplice per creare un errore è richiamare errors.New, che restituisce un nuovo errore
per un determinato messaggio di errore. L'intero pacchetto errors è lungo solo quattro righe:

errori del pacchetto

func New(text string) error { return &errorString{text} } type

errorString struct { text string }

func (e *errorString) Error() string { return e.text }

Il tipo sottostante di errorString è una struct, non una stringa, per proteggere la sua
rappresentazione da aggiornamenti involontari (o premeditati). Il motivo per cui il tipo di
puntatore *errorString, e non solo errorString, soddisfa l'interfaccia degli errori è che ogni chiamata
a New alloca un'istanza di errore distinta che non è uguale a nessun'altra. Non vogliamo che un
errore distinto, come io.EOF, venga confrontato con uno che ha semplicemente lo stesso messaggio.

fmt.Println(errors.New("EOF") == errors.New("EOF")) // "falso"

Le chiamate a errors.New sono relativamente poco frequenti perché esiste una comoda funzione wrapper,
fmt.Errorf, che esegue anche la formattazione delle stringhe. L'abbiamo usato diverse volte nel Capitolo 5.

pacchetto fmt

importazione "errori"

func Errorf(format string, args ...interface{}) error { return


errors.New(Sprintf(format, args...))
}

Sebbene *errorString sia il tipo di errore più semplice, non è l'unico. Ad esempio, il pacchetto
syscall fornisce l'API di basso livello per le chiamate di sistema di Go. Su molte piattaforme, definisce
un tipo numerico Errno che soddisfa l'errore e sulle piattaforme Unix, il metodo Error di Errno
effettua una ricerca in una tabella di stringhe, come mostrato di seguito:

pacchetto syscall

type Errno uintptr // codice di errore del sistema operativo

www.it-ebooks.info
SEZIONE 7.9. ESEMPIO: VALUTATORE DI ESPRESSIONI 197

var errors = [...]string{


1: "operazione non consentita", // EPERM
2: "no such file or directory", // ENOENT 3:
"nessun processo di questo tipo", // ESRCH
// ...
}

func (e Errno) Error() string {


if 0 <= int(e) && int(e) < len(errori) { return
errori[e]
}
return fmt.Sprintf("errno %d", e)
}

La seguente istruzione crea un valore di interfaccia con il valore Errno 2, che indica la condizione POSIX
ENOENT:

var err error = syscall.Errno(2) fmt.Println(err.Error()) // "no


such file or directory" fmt.Println(err) // "nessun file o
directory di questo tipo"

Il valore di err è mostrato graficamente nella Figura 7.6.

Figura 7.6. Un valore di interfaccia che contiene un intero syscall.Errno.

Errno è una rappresentazione efficiente degli errori delle chiamate di sistema, estratti da un insieme
finito, e soddisfa l'interfaccia standard degli errori. Vedremo altri tipi che soddisfano questa interfaccia
nella Sezione 7.11.

7.9. Esempio: Espressione Valutatore

In questa sezione, costruiremo un valutatore per semplici espressioni aritmetiche. Utilizzeremo


un'interfaccia, Expr, per rappresentare qualsiasi espressione in questo linguaggio. Per ora, questa
interfaccia non ha bisogno di metodi, ma ne aggiungeremo in seguito.
// Una Expr è un'espressione aritmetica. type
Expr interface{}

Il nostro linguaggio di espressione consiste in letterali in virgola mobile; gli operatori binari +, -, * e /; gli
operatori unari -x e +x; le chiamate di funzione pow(x,y), sin(x) e sqrt(x); variabili come x e pi; e
naturalmente le parentesi e la precedenza standard degli operatori. Tutti i valori sono di tipo float64.
Ecco alcuni esempi di espressioni:

www.it-ebooks.info
198 CAPITOLO 7. INTERFACCE

sqrt(A / pi)
pow(x, 3) + pow(y, 3)
(F - 32) * 5 / 9

I cinque tipi concreti che seguono rappresentano particolari tipi di espressione. Un Var rappresenta un
riferimento a una variabile. (Un literal rappresenta una costante in virgola mobile. I tipi unario e
binario rappresentano espressioni di operatori con uno o due operandi, che possono essere qualsiasi
tipo di Expr. Una call rappresenta una chiamata di funzione; limiteremo il campo fn a pow, sin o
sqrt.
gopl.io/ch7/eval
// Una Var identifica una variabile, ad esempio
x. type Var string
// Un letterale è una costante numerica, ad esempio
3,141. tipo literal float64
// Un unario rappresenta un'espressione di operatore unario, ad
esempio, -x. type unary struct {
op rune // uno di '+', '-' x
Expr
}
// Un binario rappresenta un'espressione di operatore binario, ad esempio,
x+y. type binary struct {
op runa // uno di '+', '-', '*', '/' x, y
Expr
}
// Una chiamata rappresenta l'espressione di una chiamata di funzione,
ad esempio, sin(x). type call struct {
fn string // uno di "pow", "sin", "sqrt" args
[]Expr
}

Per valutare un'espressione contenente delle variabili, è necessario un ambiente che mappa i nomi delle
variabili in valori:
tipo Env map[Var]float64

È necessario che ogni tipo di espressione definisca un metodo Eval che restituisca il valore
dell'espressione in un determinato ambiente. Poiché ogni espressione deve fornire questo metodo, lo
aggiungiamo all'interfaccia Expr. Il pacchetto esporta solo i tipi Expr, Env e Var; i client possono usare il
valutatore senza accedere agli altri tipi di espressione.
tipo Interfaccia Expr {
// Eval restituisce il valore di questa Expr nell'ambiente env. Eval(env Env)
float64
}

I metodi Eval concreti sono mostrati di seguito. Il metodo per Var esegue una ricerca dell'ambiente, che
restituisce zero se la variabile non è definita, mentre il metodo per literal restituisce semplicemente il
valore letterale.

www.it-ebooks.info
SEZIONE 7.9. ESEMPIO: VALUTATORE DI ESPRESSIONI 199

func (v Var) Eval(env Env) float64 { return


env[v]
}
func (l literal) Eval(_ Env) float64 {
return float64(l)
}

I metodi Eval per unario e binario valutano ricorsivamente i loro operandi, quindi applicano
l'operazione op. Non consideriamo errori le divisioni per zero o per infinito, poiché producono un
risultato, anche se non finito. Infine, il metodo call valuta gli argomenti della funzione pow, sin o
sqrt, quindi chiama la funzione corrispondente nel pacchetto math.
func (u unario) Eval(env Env) float64 {
switch u.op {
caso '+':
return +u.x.Eval(env) case
'-':
restituire -u.x.Eval(env)
}
panic(fmt.Sprintf("operatore unario non supportato: %q", u.op))
}
func (binary) Eval(env Env) float64 { switch b.op {
caso '+':
return b.x.Eval(env) + b.y.Eval(env) case
'-':
return b.x.Eval(env) - b.y.Eval(env) case
'*':
restituisce b.x.Eval(env) * b.y.Eval(env)
case '/':
restituire b.x.Eval(env) / b.y.Eval(env)
}
panic(fmt.Sprintf("operatore binario non supportato: %q", b.op))
}
func (c call) Eval(env Env) float64 { switch
c.fn {
caso "pow":
return math.Pow(c.args[0].Eval(env), c.args[1].Eval(env)) case "sin":
return math.Sin(c.args[0].Eval(env)) case
"sqrt":
restituire math.Sqrt(c.args[0].Eval(env))
}
panic(fmt.Sprintf("chiamata di funzione non supportata: %s", c.fn))
}

Molti di questi metodi possono fallire. Ad esempio, un'espressione di chiamata potrebbe avere
una funzione sconosciuta o un numero sbagliato di argomenti. È anche possibile costruire
un'espressione unaria o binaria con un operatore non valido, come ! o < (anche se la funzione Parse
citata

www.it-ebooks.info
200 CAPITOLO 7. INTERFACCE

sotto non lo farà mai). Questi errori causano il panico di Eval. Altri errori, come la valutazione di una Var
non presente nell'ambiente, fanno sì che Eval restituisca semplicemente un risultato sbagliato. Tutti
questi errori possono essere individuati ispezionando l'Expr prima di valutarla. Questo sarà il compito del
metodo Check, che mostreremo tra poco, ma prima testiamo Eval.
La funzione TestEval che segue è un test del valutatore. Utilizza il pacchetto testing, che verrà spiegato
nel Capitolo 11, ma per ora è sufficiente sapere che la chiamata a t.Errorf segnala un errore. La funzione
esegue un loop su una tabella di input che definisce tre espressioni e diversi ambienti per ciascuna di esse.
La prima espressione calcola il raggio di un cerchio data la sua area A, la seconda calcola la somma dei
cubi di due variabili x e y e la terza converte una temperatura Fahrenheit in Celsius.
func TestEval(t *testing.T) { test
:= []struct {
expr stringa
env Env vuole
stringa
}{
{"sqrt(A / pi)", Env{"A": 87616, "pi": math.Pi}, "167"},
{"pow(x, 3) + pow(y, 3)", Env{"x": 12, "y": 1}, "1729"},
{"pow(x, 3) + pow(y, 3)", Env{"x": 9, "y": 10}, "1729"},
{"5 / 9 * (F - 32)", Env{"F": -40}, "-40"},
{"5 / 9 * (F - 32)", Env{"F": 32}, "0"},
{"5 / 9 * (F - 32)", Env{"F": 212}, "100"},
}
var prevExpr stringa
per _, test := gamma di test {
// Stampa l'espressione solo quando
cambia. if test.expr != prevExpr {
fmt.Printf("\n%s\n", test.expr) prevExpr
= test.expr
}
expr, err := Parse(test.expr) if
err != nil {
t.Error(err) // errore di analisi
continua
}
got := fmt.Sprintf("%.6g", expr.Eval(test.env))
fmt.Printf("\t%v => %s\n", test.env, got)
if got != test.want {
t.Errorf("%s.Eval() in %s = %q, want %q\n",
test.expr, test.env, got, test.want)
}
}
}

Per ogni voce della tabella, il test analizza l'espressione, la valuta nell'ambiente e stampa il risultato. Non
abbiamo spazio per mostrare la funzione Parse, ma la troverete se scaricate il pacchetto con go get.

www.it-ebooks.info
SEZIONE 7.9. ESEMPIO: VALUTATORE DI ESPRESSIONI 201

Il comando go test (§11.1) esegue i test di un pacchetto:

$ go test -v gopl.io/ch7/eval

Il flag -v ci permette di vedere l'output stampato del test, che normalmente viene soppresso per un
test riuscito come questo. Ecco l'output delle istruzioni fmt.Printf del test:

sqrt(A / pi)
map[A:87616 pi:3.141592653589793] => 167

pow(x, 3) + pow(y, 3) map[x:12


y:1] => 1729 map[x:9 y:10]
=> 1729

5 / 9 * (F - 32) map[F:-
40] => -40
map[F:32] => 0
mappa[F:212] => 100

Fortunatamente gli input finora sono stati tutti ben formati, ma è improbabile che la nostra fortuna duri.
Anche nei linguaggi interpretati è consuetudine controllare la sintassi per individuare errori statici, cioè
errori che possono essere rilevati senza eseguire il programma. Separando i controlli statici da quelli
dinamici, è possibile individuare prima gli errori ed eseguire molti controlli una sola volta, anziché ogni
volta che viene valutata un'espressione.

Aggiungiamo un altro metodo all'interfaccia Expr. Il metodo Check controlla la presenza di errori statici
nell'albero della sintassi di un'espressione. Spiegheremo tra poco il suo parametro vars.

tipo Interfaccia Expr { Eval(env


Env) float64
// Check segnala gli errori in questa Expr e aggiunge i suoi vars all'insieme.
Check(vars map[Var]bool) errore
}

I metodi Check concreti sono mostrati di seguito. La valutazione di literal e Var non può fallire,
quindi i metodi Check per questi tipi restituiscono nil. I metodi per unary e binary verificano
innanzitutto che l'operatore sia valido, quindi controllano ricorsivamente gli operandi.
Analogamente, il metodo per call verifica innanzitutto che la funzione sia nota e abbia il giusto
numero di argomenti, quindi controlla ricorsivamente ogni argomento.

func (v Var) Check(vars map[Var]bool) error { vars[v]


= true
restituire nil
}

func (literal) Check(vars map[Var]bool) error { return nil


}

www.it-ebooks.info
202 CAPITOLO 7. INTERFACCE

func (u unario) Check(vars map[Var]bool) error { if


!strings.ContainsRune("+-", u.op) {
return fmt.Errorf("unexpected unary op %q", u.op)
}
restituire u.x.Check(vars)
}
func (binary) Check(vars map[Var]bool) error { if
!strings.ContainsRune("+-*/", b.op) {
return fmt.Errorf("unexpected binary op %q", b.op)
}
if err := b.x.Check(vars); err := nil { return
err
}
restituire b.y.Check(vars)
}
func (c call) Check(vars map[Var]bool) error { arity,
ok := numParams[c.fn]
if !ok {
return fmt.Errorf("funzione sconosciuta %q", c.fn)
}
if len(c.args) != arity {
return fmt.Errorf("la chiamata a %s ha %d argomenti, vuole
%d", c.fn, len(c.args), arity)
}
per _, arg := range c.args {
if err := arg.Check(vars); err := nil { return err
}
}
restituire nil
}
var numParams = map[string]int{"pow": 2, "sin": 1, "sqrt": 1}

Abbiamo elencato una selezione di input difettosi e gli errori che generano, suddivisi in due gruppi. Il Parse
(non mostrato) segnala gli errori di sintassi e la funzione Check segnala gli errori semantici.
x % 2 inaspettato '%'
math.Pi inaspettato '.'
vero inaspettato '!
"ciao" inaspettato '"
log(10) funzione sconosciuta "log"
sqrt(1, 2) la chiamata a sqrt ha 2 argomenti, ne vuole 1

L'argomento di Check, un insieme di Vars, accumula l'insieme dei nomi delle variabili presenti
nell'espressione. Ciascuna di queste variabili deve essere presente nell'ambiente perché la valutazione
abbia successo. Questo insieme è logicamente il risultato della chiamata a Check, ma poiché il metodo è
ricorsivo, è più conveniente per Check popolare un insieme passato come parametro. Il cliente deve
fornire un insieme vuoto nella chiamata iniziale.

www.it-ebooks.info
SEZIONE 7.9. ESEMPIO: VALUTATORE DI ESPRESSIONI 203

Nella Sezione 3.2 abbiamo tracciato una funzione f(x,y) fissata in fase di compilazione. Ora che possiamo
analizzare, controllare e valutare le espressioni nelle stringhe, possiamo costruire un'applicazione Web
che riceve un'espressione in tempo reale dal client e traccia la superficie di tale funzione. Possiamo usare
l'insieme vars per verificare che l'espressione sia una funzione di due sole variabili, x e y, anzi tre, dato che
forniremo r, il raggio, per comodità. Useremo il metodo Check per rifiutare le espressioni mal formate
prima dell'inizio della valutazione, in modo da non ripetere i controlli durante le 40.000 valutazioni
(100×100 celle, ognuna con quattro angoli) della funzione che seguiranno.

La funzione parseAndCheck combina queste fasi di parsing e di verifica:


gopl.io/ch7/superficie
importare "gopl.io/ch7/eval"

func parseAndCheck(s string) (eval.Expr, error) { if s == ""


{
return nil, fmt.Errorf("espressione vuota")
}
expr, err := eval.Parse(s) if
err != nil {
restituire nil, err
}
vars := make(map[eval.Var]bool)
if err := expr.Check(vars); err := nil {
return nil, err
}
per v := intervallo vars {
se v != "x" && v != "y" && v != "r" {
return nil, fmt.Errorf("variabile non definita: %s", v)
}
}
restituire expr, nil
}

Per rendere questa applicazione web, tutto ciò di cui abbiamo bisogno è la funzione plot qui sotto, che ha la
firma familiare di un http.HandlerFunc:
func plot(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
expr, err := parseAndCheck(r.Form.Get("expr")) if err
:= nil {
http.Error(w, "bad expr: "+err.Error(), http.StatusBadRequest) return
}
w.Header().Set("Content-Type", "image/svg+xml") surface(w,
func(x, y float64) float64 {
r := math.Hypot(x, y) // distanza da (0,0) return
expr.Eval(eval.Env{"x": x, "y": y, "r": r})
})
}

www.it-ebooks.info
204 CAPITOLO 7. INTERFACCE

Figura 7.7. Le superfici di tre funzioni: (a) sin(-x)*pow(1,5,-r);


(b) pow(2,sin(y))*pow(2,sin(x))/12; (c) sin(x*y/10)/10.

www.it-ebooks.info
SEZIONE 7.10. ASSERZIONI DI TIPO 205

La funzione plot analizza e controlla l'espressione specificata nella richiesta HTTP e la utilizza per creare
una funzione anonima di due variabili. La funzione anonima ha la stessa natura della funzione fissa f del
programma di plottaggio originale, ma valuta l'espressione fornita dall'utente. L'ambiente definisce x, y e
il raggio r. Infine, plot richiama surface, che è solo la funzione principale di gopl.io/ch3/surface,
modificata per prendere come parametri la funzione da tracciare e l'output io.Writer, invece di usare
la funzione fissa f e os.Stdout. La Figura 7.7 mostra tre superfici prodotte dal programma.

Esercizio 7.13: Aggiungere un metodo String a Expr per stampare l'albero della sintassi. Verificare che i
risultati, quando vengono analizzati di nuovo, producano un albero equivalente.

Esercizio 7.14: Definire un nuovo tipo concreto che soddisfi l'interfaccia Expr e fornisca una nuova
operazione, come il calcolo del valore minimo dei suoi operandi. Poiché la funzione Parse non crea
istanze di questo nuovo tipo, per utilizzarlo è necessario costruire direttamente un albero di sintassi (o
estendere il parser).

Esercizio 7.15: Scrivere un programma che legga una singola espressione dallo standard input, richieda
all'utente di fornire i valori di eventuali variabili e valuti l'espressione nell'ambiente risultante. Gestire
tutti gli errori con garbo.

Esercizio 7.16: Scrivere un programma di calcolo basato sul Web.

7.10. Tipo Asserzioni

Un'asserzione di tipo è un'operazione applicata a un valore di interfaccia. Sintatticamente, si presenta


come x.(T), dove x è un'espressione di un tipo di interfaccia e T è un tipo, chiamato tipo ''asserito''.
Un'asserzione di tipo verifica che il tipo dinamico del suo operando corrisponda al tipo asserito.

Ci sono due possibilità. In primo luogo, se il tipo asserito T è un tipo concreto, l'asserzione di tipo
verifica se il tipo dinamico di x è identico a T. Se questo controllo ha successo, il risultato dell'asserzione
di tipo è il valore dinamico di x, il cui tipo è ovviamente T. In altre parole, un'asserzione di tipo a un tipo
concreto estrae il valore concreto dal suo operando. Se il controllo fallisce, l'operazione va in panico. Per
esempio:
var w io.Writer w
= os.Stdout
f := w.(*os.File) // successo: f == os.Stdout
c := w.(*bytes.Buffer) // panico: l'interfaccia contiene *os.File, non *bytes.Buffer

In secondo luogo, se invece il tipo asserito T è un tipo di interfaccia, l'asserzione di tipo verifica se il tipo
dinamico di x soddisfa T. Se questo controllo ha successo, il valore dinamico non viene estratto; il
risultato è ancora un valore di interfaccia con lo stesso tipo e gli stessi componenti di valore, ma il
risultato ha il tipo di interfaccia T. In altre parole, un'asserzione di tipo a un tipo di interfaccia cambia il
tipo dell'espressione, rendendo accessibile un insieme diverso (e di solito più ampio) di metodi, ma
conserva il tipo dinamico e i componenti del valore all'interno del valore dell'interfaccia.

www.it-ebooks.info
206 CAPITOLO 7. INTERFACCE

Dopo la prima asserzione sul tipo, sia w che rw contengono os.Stdout e quindi hanno un tipo dinamico
di *os.File, ma w, un io.Writer, espone solo il metodo Write del file, mentre rw espone anche il metodo
Read.
var w io.Writer w
= os.Stdout
rw := w.(io.ReadWriter) // successo: *os.File ha sia lettura che scrittura
w = nuovo(ByteCounter)
rw = w.(io.ReadWriter) // panico: *ByteCounter non ha un metodo Read

Indipendentemente dal tipo asserito, se l'operando è un valore di interfaccia nil, l'asserzione sul tipo
fallisce. Un'asserzione di tipo su un tipo di interfaccia meno restrittivo (uno con meno metodi) è
raramente necessaria, poiché si comporta come un'assegnazione, tranne nel caso di nil.
w = rw // io.ReadWriter è assegnabile a io.Writer w =
rw.(io.Writer) // fallisce solo se rw == nil

Spesso non si è sicuri del tipo dinamico di un valore dell'interfaccia e si vuole verificare se è di un tipo
particolare. Se l'asserzione sul tipo compare in un'assegnazione in cui sono attesi due risultati, come le
dichiarazioni seguenti, l'operazione non va in panico in caso di fallimento, ma restituisce un secondo
risultato aggiuntivo, un booleano che indica il successo:
var w io.Writer = os.Stdout
f, ok := w.(*os.File) // successo: ok, f == os.Stdout b,
ok := w.(*bytes.Buffer) // fallimento: !ok, b == nil

Il secondo risultato viene convenzionalmente assegnato a una variabile denominata ok. Se l'operazione
fallisce, ok è false e il primo risultato è uguale al valore zero del tipo asserito, che in questo esempio è un
*bytes.Buffer nullo.
Il risultato dell'ok viene spesso utilizzato immediatamente per decidere cosa fare dopo. La forma estesa del
metodo
if rende il tutto abbastanza compatto:
if f, ok := w.(*os.File); ok {
// ...usare f...
}

Quando l'operando di un'asserzione di tipo è una variabile, piuttosto che inventare un altro nome per la
nuova variabile locale, a volte si vede il nome originale riutilizzato, in ombra rispetto all'originale, come
in questo caso:
se w, ok := w.(*os.File); ok {
// ...usare w...
}

7.11. Discriminare gli errori con le asserzioni di tipo

Consideriamo l'insieme degli errori restituiti dalle operazioni sui file nel pacchetto os. L'I/O può fallire
per diversi motivi, ma spesso tre tipi di errore devono essere gestiti in modo diverso: file già esistente
(per le operazioni di creazione), file non trovato (per le operazioni di lettura) e permesso negato.
L'opzione

www.it-ebooks.info
SEZIONE 7.11. DISCRIMINAZIONE DEGLI ERRORI CON LE ASSERZIONI DI TIPO 207

Il pacchetto os fornisce queste tre funzioni ausiliarie per classificare il guasto indicato da un dato
valore di errore:
pacchetto os

func IsExist(err error) bool func


IsNotExist(err error) bool
func IsPermission(err error) bool

Un'implementazione ingenua di uno di questi predicati potrebbe verificare che il messaggio di errore
c o n t e n g a una determinata sottostringa,
func IsNotExist(err error) bool {
// NOTA: non è robusto!
return strings.Contains(err.Error(), "il file non e s i s t e ")
}

ma poiché la logica di gestione degli errori di I/O può variare da una piattaforma all'altra, questo
approccio non è robusto e lo stesso errore può essere segnalato con una serie di messaggi di errore
diversi. Il controllo delle sottostringhe dei messaggi di errore può essere utile durante i test per garantire
che le funzioni falliscano nel modo previsto, ma è inadeguato per il codice di produzione.
Un approccio più affidabile consiste nel rappresentare i valori di errore strutturati utilizzando un tipo
dedicato. Il pacchetto os definisce un tipo chiamato PathError per descrivere i fallimenti che coinvolgono
un'operazione su un percorso di file, come Open o Delete, e una variante chiamata LinkError per
descrivere i fallimenti di operazioni che coinvolgono due percorsi di file, come Symlink e Rename.
Ecco os.PathError:
pacchetto os

// PathError registra un errore e l'operazione e il percorso del file che lo ha causato.


type PathError struct {
Stringa Op
Stringa
percorso Err
errore
}

func (e *PathError) Error() string {


restituire e.Op + " " + e.Path + ": " + e.Err.Error()
}

La maggior parte dei client ignora PathError e gestisce tutti gli errori in modo uniforme chiamando i loro
metodi Error. Sebbene il metodo Error di PathError formi un messaggio semplicemente catenando
i campi, la struttura di PathError conserva i componenti sottostanti dell'errore. I client che hanno bisogno
di distinguere un tipo di errore da un altro possono usare un'asserzione di tipo per rilevare il tipo
specifico dell'errore; il tipo specifico fornisce maggiori dettagli rispetto a una semplice stringa.
_, err := os.Open("/no/such/file")
fmt.Println(err) // "open /no/such/file: No such file or directory" fmt.Printf("%#v\n", err)
// Uscita:
// &os.PathError{Op: "open", Path:"/no/such/file", Err:0x2}

www.it-ebooks.info
208 CAPITOLO 7. INTERFACCE

Ecco come funzionano le tre funzioni di aiuto. Ad esempio, IsNotExist, mostrata di seguito, segnala se un
errore è uguale a syscall.ENOENT (§7.8) o all'errore distinto os.ErrNotExist (vedere io.EOF nel
§5.4.2), oppure se è un *PathError il cui errore sottostante è uno di questi due.
importare (
"errori"
"syscall"
)
var ErrNotExist = errors.New("il file non e s i s t e ")
// IsNotExist restituisce un booleano che indica se l'errore è noto a
// segnala che un file o una directory non e s i s t e . È soddisfatta da
// ErrNotExist e alcuni errori di syscall. func
IsNotExist(err error) bool {
if pe, ok := err.(*PathError); ok { err =
pe.Err
}
return err == syscall.ENOENT || err == ErrNotExist
}

Ed eccola in azione:
_, err := os.Open("/no/such/file")
fmt.Println(os.IsNotExist(err)) // "vero"

Naturalmente, la struttura di PathError viene persa se il messaggio di errore viene combinato in una
stringa più grande, ad esempio tramite una chiamata a fmt.Errorf. La discriminazione degli errori di
solito deve essere fatta subito dopo l'operazione fallita, prima che l'errore venga propagato al
chiamante.

7.12. Interrogare i comportamenti con le asserzioni del tipo di interfaccia

La logica sottostante è simile alla parte del server web net/http responsabile della scrittura di
campi di intestazione HTTP come "Content-type: text/html". Il file io.Writer w rappresenta la
risposta HTTP; i byte scritti su di esso vengono inviati al browser web di qualcuno.
func writeHeader(w io.Writer, contentType string) error {
if _, err := w.Write([]byte("Content-Type: ")); err != nil { return
err
}
if _, err := w.Write([]byte(contentType)); err := nil { return
err
}
// ...
}

Poiché il metodo Write richiede una slice di byte e il valore che vogliamo scrivere è una stringa, è
necessaria una conversione []byte(...). Questa conversione alloca memoria e crea una copia, che però
viene gettata via quasi subito dopo. Facciamo finta che questa sia una parte fondamentale di

www.it-ebooks.info
SEZIONE 7.12. INTERROGAZIONE DEI COMPORTAMENTI CON LE ASSERZIONI SUI TIPI DI 209
INTERFACCIA

del server web e che la nostra profilazione ha rivelato che questa allocazione di memoria lo sta rallentando.
Possiamo evitare di allocare memoria in questo punto?

L'interfaccia io.Writer ci dice solo una cosa sul tipo concreto che w contiene: che v i si possono scrivere
byte. Se guardiamo dietro le quinte del pacchetto net/http, vediamo che il tipo dinamico che w contiene in
questo programma ha anche un metodo WriteString che consente di scrivervi in modo efficiente stringhe,
evitando di allocare una copia temporanea. (Questo può sembrare un colpo nel buio, ma un certo
numero di tipi importanti che soddisfano io.Writer hanno anche un metodo WriteString, tra cui
*bytes.Buffer, *os.File e *bufio.Writer).

Non possiamo assumere che un arbitrario io.Writer w abbia anche il metodo WriteString. Possiamo
però definire una nuova interfaccia che abbia solo questo metodo e usare un'asserzione di tipo per
verificare se il tipo dinamico di w soddisfa questa nuova interfaccia.

// writeString scrive s in w.
// Se w ha un metodo WriteString, questo viene invocato al posto di w.Write.
func writeString(w io.Writer, s string) (n int, err error) {
type stringWriter interface {
WriteString(string) (n int, err error)
}
if sw, ok := w.(stringWriter); ok {
return sw.WriteString(s) // evita una copia
}
return w.Write([]byte(s)) // alloca una copia temporanea
}

func writeHeader(w io.Writer, contentType string) error {


if _, err := writeString(w, "Content-Type: "); err != nil { return
err
}
if _, err := writeString(w, contentType); err := nil { return err
}
// ...
}

Per evitare di r i p e t e r c i , abbiamo spostato il controllo nella funzione di utilità writeString, ma è così
utile che la libreria standard la fornisce come io.WriteString. È il modo consigliato per scrivere una
stringa in un writer io.Writer.

L'aspetto curioso di questo esempio è che non esiste un'interfaccia standard che definisca il metodo
WriteString e ne specifichi il comportamento richiesto. Inoltre, il fatto che un tipo creato soddisfi o
meno l'interfaccia stringWriter è determinato solo dai suoi metodi, non da alcuna relazione dichiarata
tra esso e il tipo dell'interfaccia. Ciò significa che la tecnica sopra descritta si basa sul presupposto che se
un tipo soddisfa l'interfaccia sottostante, allora WriteString(s) deve avere lo stesso effetto di
Write([]byte(s)).

www.it-ebooks.info
210 CAPITOLO 7. INTERFACCE

interfaccia {
io.Writer
WriteString(s stringa) (n int, err error)
}

Sebbene io.WriteString documenti la sua assunzione, è probabile che poche funzioni che la chiamano
documentino che anche loro fanno la stessa assunzione. La definizione di un metodo di un particolare
tipo viene considerata come un assenso implicito a un certo contratto comportamentale. I neofiti di Go,
soprattutto quelli che provengono da un background di linguaggi fortemente tipizzati, possono trovare
inquietante questa mancanza di intenzionalità esplicita, ma in pratica è raramente un problema. Con
l'eccezione dell'interfaccia vuota interface{}, i tipi di interfaccia sono raramente soddisfatti da
coincidenze involontarie.
La funzione writeString di cui sopra utilizza un'asserzione di tipo per verificare se un valore di
un tipo di interfaccia generale soddisfa anche un tipo di interfaccia più specifico e, in caso affermativo,
utilizza i comportamenti dell'interfaccia specifica. Questa tecnica può essere utilizzata sia che l'interfaccia
interrogata sia standard come io.ReadWriter o definita dall'utente come stringWriter.
È anche il modo in cui fmt.Fprintf distingue i valori che soddisfano error o fmt.Stringer da tutti gli
altri valori. All'interno di fmt.Fprintf, c'è un passo che converte un singolo operando in una stringa,
qualcosa di simile a questo:
pacchetto fmt

func formatOneValue(x interface{}) string { if


err, ok := x.(error); ok {
restituire err.Error()
}
if str, ok := x.(Stringer); ok { return
str.String()
}
// ...tutti gli altri tipi...
}

Se x soddisfa una delle due interfacce, ciò determina la formattazione del valore. In caso contrario, il caso
predefinito gestisce tutti gli altri tipi in modo più o meno uniforme utilizzando la riflessione; scopriremo
come nel Capitolo 12.
Ancora una volta, questo presuppone che qualsiasi tipo con un metodo String soddisfi il contratto
comportamentale di fmt.Stringer, che è quello di restituire una stringa adatta alla stampa.

7.13. Tipo Interruttori

Le interfacce sono utilizzate in due stili distinti. Nel primo stile, esemplificato da io.Reader,
io.Writer, fmt.Stringer, sort.Interface, http.Handler ed error, i metodi di un'interfaccia esprimono le
somiglianze dei tipi concreti che soddisfano l'interfaccia, ma nascondono i dettagli di rappresentazione e
le operazioni intrinseche di tali tipi concreti. L'enfasi è sui metodi, non sui tipi concreti.

www.it-ebooks.info
SEZIONE 7.13. INTERRUTTORI DI 211
TIPO

Il secondo stile sfrutta la capacità di un valore di interfaccia di contenere valori di una varietà di tipi
concreti e considera l'interfaccia come l'unione di questi tipi. Le asserzioni sui tipi vengono utilizzate per
discriminare dinamicamente tra questi tipi e trattare ogni caso in modo diverso. In questo stile, l'enfasi è
sui tipi concreti che soddisfano l'interfaccia, non sui metodi dell'interfaccia (se ne ha), e non c'è alcun
occultamento di informazioni. Descriveremo le interfacce utilizzate in questo modo come unioni
discriminate.
Chi ha familiarità con la programmazione orientata agli oggetti può riconoscere questi due stili come
polimorfismo dei sottotipi e polimorfismo ad hoc, ma non è necessario ricordare questi termini. Nel resto
del capitolo presenteremo esempi del secondo stile.
Le API di Go per l'interrogazione di un database SQL, come quelle di altri linguaggi, ci permettono di
separare in modo netto la parte fissa di una query dalle parti variabili. Un esempio di client potrebbe
assomigliare a questo:
importare "database/sql"

func listTracks(db sql.DB, artist string, minYear, maxYear int) { result, err :=
db.Exec(
"SELECT * FROM tracks WHERE artist = ? AND ? <= year AND year <= ?", artist, minYear,
maxYear)
// ...
}

Il metodo Exec sostituisce ogni '?' nella stringa della query con un letterale SQL che indica il valore
dell'argomento corrispondente, che può essere un booleano, un numero, una stringa o nil. Costruire le
query in questo modo aiuta a evitare gli attacchi SQL injection, in cui un avversario prende il controllo
della query sfruttando la citazione impropria dei dati di input. All'interno di Exec si può trovare una
funzione come quella che segue, che converte ogni valore dell'argomento nella sua notazione letterale
SQL.
func sqlQuote(x interface{}) string { if x
== nil {
restituire "NULL"
} else if _, ok := x.(int); ok { return
fmt.Sprintf("%d", x)
} else if _, ok := x.(uint); ok { return
fmt.Sprintf("%d", x)
} else if b, ok := x.(bool); ok { if b {
restituire "VERO"
}
restituire "FALSO"
} else if s, ok := x.(stringa); ok {
return sqlQuoteString(s) // (non mostrato)
} else {
panic(fmt.Sprintf("tipo inatteso %T: %v", x, x))
}
}

Un'istruzione switch semplifica una catena if-else che esegue una serie di test di uguaglianza dei
valori. Un'analoga istruzione switch semplifica una catena if-else di asserzioni di tipo.

www.it-ebooks.info
212 CAPITOLO 7. INTERFACCE

Nella sua forma più semplice, uno switch di tipo assomiglia a una normale istruzione switch in cui
l'oper- e è x.(tipo) - che è letteralmente la parola chiave type - e ogni caso ha uno o più tipi. Uno switch di
tipo consente un ramo a più vie basato sul tipo dinamico del valore dell'interfaccia. Il caso nil
corrisponde se x == nil e il caso default corrisponde se nessun altro caso corrisponde. Un
interruttore di tipo per sqlQuote avrebbe questi casi:
switch x.(tipo) { caso nil:
// ...
caso int, uint: // ...
case bool: // ...
case string: // ...
default: // ...
}

Come in una normale istruzione switch (§1.8), i casi vengono considerati in ordine e, quando viene
trovata una corrispondenza, viene eseguito il corpo del caso. L'ordine dei casi diventa significativo
quando uno o più tipi di casi sono interfacce, poiché in questo caso c'è la possibilità che due casi
corrispondano. La posizione del caso predefinito rispetto agli altri è irrilevante. Non è consentito il
fallthrough.

Si noti che nella funzione originale, la logica per i casi bool e string ha bisogno di accedere al valore
estratto dall'asserzione di tipo. Poiché questo è tipico, l'istruzione type switch ha una forma estesa che
lega il valore estratto a una nuova variabile all'interno di ciascun caso:
switch x := x.(tipo) { /* ... */ }

Anche qui abbiamo chiamato le nuove variabili x; come per le asserzioni di tipo, il riutilizzo dei
nomi delle variabili è comune. Come un'asserzione switch, un type switch crea implicitamente un
blocco lessicale, quindi la dichiarazione della nuova variabile x non entra in conflitto con una variabile x
in un blocco esterno. Anche ogni caso crea implicitamente un blocco lessicale separato.

La riscrittura di sqlQuote per utilizzare la forma estesa di type switch rende il tutto molto più chiaro:
func sqlQuote(x interface{}) string { switch
x := x.(type) {
caso nil:
restituire
"NULL" caso int,
uint:
return fmt.Sprintf("%d", x) // x ha il tipo interface{} qui. case bool:
se x {
restituire "VERO"
}
restituire "FALSO"
caso stringa:
return sqlQuoteString(x) // (non mostrato)
predefinito:
panic(fmt.Sprintf("tipo inatteso %T: %v", x, x))
}
}

www.it-ebooks.info
SEZIONE 7.14. ESEMPIO: DECODIFICA XML BASATA SU TOKEN 213

In questa versione, all'interno del blocco di ogni caso di tipo singolo, la variabile x ha lo stesso tipo del
caso. Ad esempio, x ha il tipo bool nel caso bool e string nel caso string. In tutti gli altri casi, x ha
il tipo (interfaccia) dell'operando switch, che in questo esempio è inter- face{}. Quando la
stessa azione è richiesta per più casi, come int e uint, l'interruttore di tipo consente di combinarli
facilmente.
Sebbene sqlQuote accetti un argomento di qualsiasi tipo, la funzione viene eseguita fino al
completamento solo se il tipo dell'argomento corrisponde a uno dei casi previsti dal selettore di tipo; in
caso contrario, si blocca con un messaggio di ''tipo inatteso''. Sebbene il tipo di x sia interface{}, lo
consideriamo un'unione discriminata di int, uint, bool, string e nil.

7.14. Esempio: Decodifica XML basata su token

La sezione 4.5 ha mostrato come decodificare i documenti JSON in strutture dati Go con le funzioni
Marshal e Unmarshal del pacchetto encoding/json. Il pacchetto encoding/xml fornisce un'API simile.
Questo approccio è comodo quando si vuole costruire una rappresentazione dell'albero dei
documenti, ma non è necessario per molti programmi. Il pacchetto encoding/xml fornisce anche
un'API di livello inferiore basata sui token per la decodifica di XML. Nello stile basato sui token, il
parser consuma l'input e produce un flusso di token, principalmente di quattro tipi: InizioElemento,
FineElemento, CharData e Commento, ognuno dei quali è un tipo concreto del pacchetto encoding/xml.
Ogni chiamata a (*xml.Decoder).Token restituisce un token.
Qui di seguito sono riportate le parti rilevanti dell'API:
codifica/xml
pacchetto xml

type Name struct {


Stringa locale // ad esempio, "Titolo" o "id".
}

type Attr struct { // ad esempio, name="valore"


Name Name
Stringa di valore
}

// Un token include StartElement, EndElement, CharData,


// e Comment, più alcuni tipi esoterici (non mostrati). type Token
interface{}
type StartElement struct { // ad esempio, <nome>
Nome Nome
Attr []Attr
}
tipo EndElement struct { Nome Nome } // ad esempio, </name>
tipo CharData []byte // ad esempio, <p>CharData</p>
tipo Commento []byte // ad esempio, <!-- Comment
--> type Decoder struct{ /* ... */ }

www.it-ebooks.info
214 CAPITOLO 7. INTERFACCE

func NewDecoder(io.Reader) *Decoder


func (*Decoder) Token() (Token, error) // restituisce il prossimo Token nella sequenza

Anche l'interfaccia Token, che non ha metodi, è un esempio di unione discriminata. Lo scopo di
un'interfaccia tradizionale come io.Reader è quello di nascondere i dettagli dei tipi concreti che la
soddisfano, in modo da poter creare nuove implementazioni; ogni tipo concreto viene trattato in modo
uniforme. Al contrario, l'insieme dei tipi concreti che soddisfano un'unione discriminata è fissato
dal progetto e viene esposto, non nascosto. I tipi di unione discriminata hanno pochi metodi; le funzioni
che operano su di essi sono espresse come un insieme di casi utilizzando un interruttore di tipo, con una
logica diversa per ogni caso.

Il programma xmlselect qui sotto estrae e stampa il testo che si trova sotto determinati elementi in un
albero di documenti XML. Utilizzando l'API di cui sopra, può svolgere il suo lavoro in un solo passaggio
sull'input senza mai materializzare l'albero.
gopl.io/ch7/xmlselect
// Xmlselect stampa il testo degli elementi selezionati di un documento XML. pacchetto
main

importare (
"encoding/xml"
"fmt"
"io"
"os"
"stringhe"
)

func main() {
dec := xml.NewDecoder(os.Stdin)
var stack []string // stack di nomi di elementi per {
tok, err := dec.Token() if
err == io.EOF {
pausa
} else if err != nil {
fmt.Fprintf(os.Stderr, "xmlselect: %v\n", err) os.Exit(1)
}
switch tok := tok.(type) { case
xml.StartElement:
stack = append(stack, tok.Name.Local) // push case
xml.EndElement:
stack = stack[:len(stack)-1] // pop case
xml.CharData:
if containsAll(stack, os.Args[1:]) {
fmt.Printf("%s: %s\n", strings.Join(stack, " "), tok)
}
}
}
}

www.it-ebooks.info
SEZIONE 7.14. ESEMPIO: DECODIFICA XML BASATA SU TOKEN 215

// containsAll segnala se x contiene gli elementi di y, in ordine. func


containsAll(x, y []string) bool {
for len(y) <= len(x) { if
len(y) == 0 {
restituire vero
}
if x[0] == y[0] { y
= y[1:]
}
x = x[1:]
}
restituire false
}

Ogni volta che il ciclo in main incontra uno StartElement, spinge il nome dell'elemento su una pila e per
ogni EndElement estrae il nome dalla pila. L'API garantisce che la sequenza di token di StartElement e
EndElement sia correttamente abbinata, anche in documenti mal formati. I commenti vengono
ignorati. Quando xmlselect incontra un CharData, stampa il testo solo se la pila contiene tutti gli
elementi nominati dagli argomenti della riga di comando, in ordine.
Il comando seguente stampa il testo di tutti gli elementi h2 che appaiono sotto due livelli di div
elementi. Il suo input è la specifica XML, essa stessa un documento XML.
$ go build gopl.io/ch1/fetch
$ ./fetch http://www.w3.org/TR/2006/REC-xml11-20060816 |
./xmlselect div div h2
html body div div h2: 1 Introduzione html
body div div h2: 2 Documenti
html body div div h2: 3 Strutture logiche html body
div div h2: 4 Strutture fisiche html body div div h2: 5
Conformità
html body div div h2: 6 Notazione html body
div div h2: A Riferimenti
html body div div h2: B Definizioni per la normalizzazione dei caratteri
...

Esercizio 7.17: Estendere xmlselect in modo che gli elementi possano essere selezionati non solo per
nome, ma a n c h e per i loro attributi, alla maniera dei CSS, in modo c h e , per esempio, un
elemento come
<div id="page" class="wide"> può essere selezionato in base a un id o a una classe
corrispondente, oltre che al suo nome.
Esercizio 7.18: Utilizzando l'API di decodifica basata sui token, scrivere un programma che legga un
documento XML arbitrario e costruisca un albero di nodi generici che lo rappresenti. I nodi sono di due
tipi: I nodi CharData rappresentano stringhe di testo, mentre i nodi Element rappresentano elementi
denominati e i loro attributi. Ogni nodo elemento ha una serie di nodi figli.
Le seguenti dichiarazioni possono essere utili.
importare "encoding/xml"

www.it-ebooks.info
216 CAPITOLO 7. INTERFACCE

tipo Node interface{} // CharData o *Elemento tipo


CharData stringa
tipo Elemento struct {
Tipo xml.Name
Attr []xml.Attr
Bambini []Nodo
}

7.15. Alcuni consigli di

Quando progettano un nuovo pacchetto, i programmatori di Go alle prime armi spesso iniziano creando
un insieme di interfacce e solo successivamente definiscono i tipi concreti che le soddisfano. Questo
approccio dà luogo a molte interfacce, ognuna delle quali ha una sola implementazione. Non fatelo.
Queste interfacce sono astrazioni inutili e hanno anche un costo di esecuzione. È possibile limitare i
metodi di un tipo o i campi di una struct che sono visibili al di fuori di un pacchetto utilizzando il
meccanismo di esportazione (§6.6). Le interfacce sono necessarie solo quando ci sono due o più tipi
concreti che devono essere trattati in modo uniforme.
Facciamo un'eccezione a questa regola quando un'interfaccia è soddisfatta da un singolo tipo concreto,
ma questo tipo non può vivere nello stesso pacchetto dell'interfaccia a causa delle sue dipendenze. In
questo caso, un'interfaccia è un buon modo per disaccoppiare due pacchetti.
Poiché le interfacce sono utilizzate in Go solo quando sono soddisfatte da due o più tipi, esse astraggono
necessariamente dai dettagli di ogni particolare implementazione. Il risultato sono interfacce più piccole
con un numero minore di metodi semplici, spesso uno solo, come nel caso di io.Writer o fmt.Stringer.
Le interfacce piccole sono più facili da soddisfare quando arrivano nuovi tipi. Una buona r e g o l a
per la progettazione di interfacce è chiedere solo ciò di cui si ha bisogno.
Questo conclude il nostro tour dei metodi e delle interfacce. Go offre un ottimo supporto per lo
stile di programmazione orientato agli oggetti, ma questo non significa che sia necessario utilizzarlo
esclusivamente. Non è necessario che tutto sia un oggetto; le funzioni indipendenti hanno il loro
posto, così come i tipi di dati non incapsulati. Osservate che gli esempi dei primi cinque capitoli di
questo libro chiamano complessivamente non più di due dozzine di metodi, come input.Scan,
rispetto alle normali chiamate di funzione come fmt.Printf.

www.it-ebooks.info
8
Goroutine e canali
La programmazione concorrente, ovvero l'espressione di un programma come composizione di diverse
attività autonome, non è mai stata così importante come oggi. I server Web gestiscono le richieste di
migliaia di client contemporaneamente. Le applicazioni per tablet e telefoni eseguono il rendering di
animazioni nell'interfaccia utente e contemporaneamente eseguono calcoli e richieste di rete nel back
ground. Anche i tradizionali problemi batch (lettura di alcuni dati, calcolo, scrittura di un output)
utilizzano la concorrenza per nascondere la latenza delle operazioni di I/O e per sfruttare i numerosi
processori dei computer moderni, che ogni anno aumentano di numero ma non di velocità.
Go consente due stili di programmazione concorrente. Questo capitolo presenta le goroutine e i canali,
che supportano i processi sequenziali comunicanti o CSP, un modello di concorrenza in cui i valori
vengono passati tra attività indipendenti (goroutine) ma le variabili sono per lo più confinate a una
singola attività. Il Capitolo 9 tratta alcuni aspetti del modello più tradizionale di multithreading a
memoria condivisa, che vi sarà familiare se avete usato i thread in altri linguaggi mainstream. Il capitolo
9 evidenzia anche alcuni importanti rischi e insidie della programmazione concorrente che non
verranno approfonditi in questo capitolo.
Anche se il supporto di Go per la concomitanza è uno dei suoi grandi punti di forza, ragionare sui
programmi concomitanti è intrinsecamente più difficile che su quelli sequenziali e le intuizioni acquisite
con la programmazione sequenziale possono a volte portarci fuori strada. Se questo è il vostro primo
incontro con la concorrenza, vi consigliamo di dedicare un po' di tempo in più a riflettere sugli esempi di
questi due capitoli.

8.1. Gorotoine

In Go, ogni attività in esecuzione simultanea è chiamata goroutine. Consideriamo un programma con
due funzioni, una che esegue dei calcoli e una che scrive degli output, e supponiamo che nessuna delle
due funzioni chiami l'altra. Un programma sequenziale può chiamare una funzione e poi

217

www.it-ebooks.info
218 CAPITOLO 8. GOROUTINE E CANALI

ma in un programma concorrente con due o più goroutine, le chiamate a entrambe le funzioni possono
essere attive contemporaneamente. Vedremo un programma di questo tipo tra poco.
Se avete usato i thread del sistema operativo o i thread in altri linguaggi, per ora potete pensare che una
goroutine sia simile a un thread e sarete in grado di scrivere programmi corretti. Le differenze tra thread
e goroutine sono essenzialmente quantitative, non qualitative, e saranno descritte nella Sezione 9.8.
All'avvio di un programma, la sua unica goroutine è quella che chiama la funzione main, quindi la
chiamiamo goroutine main. Le nuove goroutine vengono create dall'istruzione go. Dal punto di vista
sintattico, una dichiarazione go è una normale chiamata a una funzione o a un metodo preceduta dalla
parola chiave go. L'istruzione go fa sì che la funzione venga chiamata in una nuova goroutine creata.
L'istruzione go stessa si chiude immediatamente:
f() // chiamare f(); attendere il suo ritorno
go f() // crea una nuova goroutine che chiama f(); non aspettare

Nell'esempio che segue, la goroutine principale calcola il 45° numero di Fibonacci. Poiché utilizza un
algoritmo ricorsivo terribilmente inefficiente, il programma viene eseguito per un tempo considerevole,
durante il quale vorremmo fornire all'utente un'indicazione visiva del fatto che il programma è ancora in
esecuzione, visualizzando uno ''spinner'' testuale animato.
gopl.io/ch8/spinner
func main() {
go spinner(100 * time.Millisecond) const n =
45
fibN := fib(n) // lento fmt.Printf("\rFibonacci(%d) =
%d\n", n, fibN)
}

func spinner(delay time.Duration) { for {


per _, r := intervallo `-\|/` {
fmt.Printf("\r%c", r)
time.Sleep(delay)
}
}
}

func fib(x int) int { if x


<2{
restituire x
}
restituire fib(x-1) + fib(x-2)
}

Dopo alcuni secondi di animazione, la chiamata fib(45) ritorna e la funzione principale stampa il suo
risultato:
Fibonacci(45) = 1134903170

www.it-ebooks.info
SEZIONE 8.2. ESEMPIO: SERVER DELL'OROLOGIO 219
CONCORRENTE

La funzione principale ritorna. In questo caso, tutte le goroutine vengono interrotte bruscamente e il
programma esce. Oltre al ritorno di main o all'uscita dal programma, non esiste un modo programmatico
per fermare una goroutine, ma come vedremo più avanti, esistono modi per comunicare con una
goroutine e chiedere che si fermi da sola.

Si noti come il programma sia espresso come la composizione di due attività autonome, la rotazione e il
calcolo di Fibonacci. Ciascuna di esse è scritta come funzione separata, ma entrambe progrediscono
simultaneamente.

8.2. Esempio: Orologio concorrente Server

La rete è un ambito naturale in cui utilizzare la concorrenza, poiché i server gestiscono in genere
molte connessioni dai loro client contemporaneamente, mentre ogni client è essenzialmente
indipendente dagli altri. In questa sezione introdurremo il pacchetto net, che fornisce i componenti
per costruire programmi client e server in rete che comunicano su socket TCP, UDP o dominio
Unix. Il pacchetto net/http, che utilizziamo dal Capitolo 1, è costruito sulla base delle funzioni del
pacchetto net.

Il primo esempio è un server orologio sequenziale che scrive l'ora corrente al client una volta al secondo:
gopl.io/ch8/clock1
// Clock1 è un server TCP che scrive periodicamente l'ora. pacchetto
main

importare (
"io"
"log"
"rete"
"tempo"
)

func main() {
listener, err := net.Listen("tcp", "localhost:8000") if err :=
nil {
log.Fatal(err)
}
per {
conn, err := listener.Accept() if
err := nil {
log.Print(err) // ad esempio, connessione
interrotta continua
}
handleConn(conn) // gestisce una connessione alla volta
}
}

www.it-ebooks.info
220 CAPITOLO 8. GOROUTINE E CANALI

func handleConn(c net.Conn) { defer


c.Close()
per {
_, err := io.WriteString(c, time.Now().Format("15:04:05\n")) if err !=
nil {
return // ad esempio, client disconnesso
}
time.Sleep(1 * time.Second)
}
}

La funzione Listen crea un net.Listener, un oggetto che ascolta le connessioni in arrivo su una
porta di rete, in questo caso la porta TCP localhost:8000. Il metodo Accept dell'ascoltatore blocca
fino a quando non viene effettuata una richiesta di connessione in entrata, quindi restituisce un
oggetto net.Conn che rappresenta la connessione.
La funzione handleConn gestisce una connessione client completa. In un ciclo, scrive l'ora corrente,
time.Now(), al client. Poiché net.Conn soddisfa l'interfaccia io.Writer, possiamo scrivere direttamente
su di esso. Il ciclo termina quando la scrittura fallisce, probabilmente perché il client si è scollegato;
a questo punto handleConn chiude la sua parte di connessione con una chiamata differita a Close e torna
ad aspettare un'altra richiesta di connessione.
Il metodo time.Time.Format fornisce un modo per formattare le informazioni su data e ora in base a
un esempio. Il suo argomento è un modello che indica come formattare un orario di riferimento, in
particolare Mon Jan 2 03:04:05PM 2006 UTC-0700. L'ora di riferimento ha otto componenti (giorno
della settimana, mese, giorno del mese e così via). Qualsiasi insieme di questi componenti può
apparire nella stringa Formato in qualsiasi ordine e in diversi formati; i componenti selezionati della
data e dell'ora saranno visualizzati nei formati selezionati. In questo caso utilizziamo solo l'ora, i minuti e
i secondi dell'ora. Il pacchetto time definisce modelli per molti formati di tempo standard, come
time.RFC1123. Lo stesso meccanismo viene utilizzato al contrario quando si analizza un orario
utilizzando time.Parse.
Per connettersi al server, è necessario un programma client come nc (''netcat''), un programma di utilità
standard per manipolare le connessioni di rete:
$ go build gopl.io/ch8/clock1
$ ./clock1 &
$ nc localhost 8000
13:58:54
13:58:55
13:58:56
13:58:57
^C

Il client visualizza l'ora inviata dal server ogni secondo, fino a quando non viene interrotto con Control-
C, che nei sistemi Unix viene riprodotto come ^C dalla shell. Se nc o netcat non sono installati sul vostro
sistema, potete usare telnet o questa semplice versione Go di netcat che usa net.Dial per connettersi a
un server TCP:

www.it-ebooks.info
SEZIONE 8.2. ESEMPIO: SERVER DELL'OROLOGIO 221
CONCORRENTE

gopl.io/ch8/netcat1
// Netcat1 è un client TCP di sola lettura.
pacchetto main

importare (
"io"
"log"
"rete"
"os"
)

func main() {
conn, err := net.Dial("tcp", "localhost:8000") if err
:= nil {
log.Fatal(err)
}
deferisci conn.Close() mustCopy(os.Stdout,
conn)
}

func mustCopy(dst io.Writer, src io.Reader) { if _, err :=


io.Copy(dst, src); err := nil {
log.Fatal(err)
}
}

Questo programma legge i dati dalla connessione e li scrive sullo standard output finché non si verifica
una condizione di fine file o un errore. La funzione mustCopy è un'utilità utilizzata in diversi esempi di
questa sezione. Eseguiamo due client contemporaneamente su terminali diversi, uno mostrato a sinistra
e uno a destra:

$ go build gopl.io/ch8/netcat1
$ ./netcat1
13:58:54 $ ./netcat1
13:58:55
13:58:56
^C
13:58:57
13:58:58
13:58:59
^C
$ killall clock1

Il comando killall è un'utilità Unix che uccide tutti i processi con il nome indicato.

Il secondo client deve aspettare che il primo client abbia finito, perché il server è sequenziale e tratta solo
un client alla volta. È necessaria solo una piccola modifica per rendere il server con- corrente: l'aggiunta
della parola chiave go alla chiamata a handleConn fa sì che ogni chiamata venga eseguita nella propria
goroutine.

www.it-ebooks.info
222 CAPITOLO 8. GOROUTINE E CANALI

gopl.io/ch8/clock2
per {
conn, err := listener.Accept() if
err := nil {
log.Print(err) // ad esempio, connessione interrotta
continua
}
go handleConn(conn) // gestisce le connessioni in modo concorrente
}

Ora, più clienti possono ricevere l'orario contemporaneamente:


$ go build gopl.io/ch8/clock2
$ ./clock2 &
$ go build gopl.io/ch8/netcat1
$ ./netcat1
14:02:54 $ ./netcat1
14:02:55 14:02:55
14:02:56 14:02:56
14:02:57 ^C
14:02:58
14:02:59 $ ./netcat1
14:03:00 14:03:00
14:03:01 14:03:01
^C 14:03:02
^C
$ killall clock2

Esercizio 8.1: Modificate clock2 per accettare un numero di porta e scrivete un programma,
clockwall, che agisca come client di diversi server di orologi contemporaneamente, leggendo gli orari da
ciascuno di essi e visualizzando i risultati in una tabella, simile al muro di orologi che si vede in
alcuni uffici aziendali. Se avete accesso a computer distribuiti geograficamente, eseguite le istanze in
remoto; altrimenti eseguite istanze locali su porte diverse con fusi orari falsi.
$ TZ=US/Eastern ./clock2 -port 8010 &
$ TZ=Asia/Tokyo ./clock2 -port 8020 &
$ TZ=Europa/Londra ./clock2 -port 8030 &
$ clockwall NewYork=localhost:8010 Londra=localhost:8020 Tokyo=localhost:8030

Esercizio 8.2: Implementare un server FTP (File Transfer Protocol) concorrente. Il server deve
interpretare i comandi di ciascun client, come cd per cambiare directory, ls per elencare una directory,
get per inviare il contenuto di un file e close per chiudere la connessione. È possibile utilizzare il
comando ftp standard come client, oppure scriverne uno proprio.

8.3. Esempio: Server Echo concorrente

Il server dell'orologio utilizzava una goroutine per connessione. In questa sezione, costruiremo un server
di eco che utilizza più goroutine per connessione. La maggior parte dei server di eco si limita a scrivere
qualsiasi cosa

www.it-ebooks.info
SEZIONE 8.3. ESEMPIO: SERVER ECO CONCORRENTE 223

che può essere eseguita con questa versione banale di handleConn:


func handleConn(c net.Conn) {
io.Copy(c, c) // NOTA: ignorare gli errori
c.Close()
}

Un server di eco più interessante potrebbe simulare il riverbero di un'eco reale, con la risposta forte
all'inizio ("HELLO!"), poi moderata ("Hello!") dopo un ritardo, quindi silenziosa ("hello!") prima
di svanire nel nulla, come in questa versione di handleConn:
gopl.io/ch8/reverb1
func echo(c net.Conn, shout string, delay time.Duration) {
fmt.Fprintln(c, "\t", strings.ToUpper(shout)) time.Sleep(delay)
fmt.Fprintln(c, "\t", shout) time.Sleep(delay)
fmt.Fprintln(c, "\t", strings.ToLower(shout))
}

func handleConn(c net.Conn) { input :=


bufio.NewScanner(c) for
input.Scan() {
echo(c, input.Text(), 1*time.Second)
}
// NOTA: ignorare i potenziali errori di input.Err() c.Close()
}

Dovremo aggiornare il nostro programma client in modo che invii l'input del terminale al server e allo
stesso tempo copi la risposta del server nell'output, il che rappresenta un'altra opportunità per utilizzare
la concorrenza:
gopl.io/ch8/netcat2
func main() {
conn, err := net.Dial("tcp", "localhost:8000") if err
:= nil {
log.Fatal(err)
}
rinviare conn.Close()
go mustCopy(os.Stdout, conn) mustCopy(conn,
os.Stdin)
}

Mentre la goroutine principale legge l'input standard e lo invia al server, una seconda goroutine legge e
stampa la risposta del server. Quando la goroutine principale incontra la fine dell'input, ad esempio
dopo che l'utente ha digitato Control-D (^D) sul terminale (o l'equivalente Control-Z su Microsoft
Windows), il programma si ferma, anche se l'altra goroutine deve ancora lavorare. (Vedremo come far sì
che il programma attenda la fine di entrambe le goroutine una volta introdotti i canali nella Sezione
8.4.1).

www.it-ebooks.info
224 CAPITOLO 8. GOROUTINE E CANALI

Nella sessione sottostante, l'input del client è allineato a sinistra e le risposte del server sono indentate. Il
client grida al server eco tre volte:

$ go build gopl.io/ch8/reverb1
$ ./reverb1 &
$ go build gopl.io/ch8/netcat2
$ ./netcat2
Pronto?
PRONTO?
Pronto?
Pronto?
C'è qualcuno lì?
C'È QUALCUNO LÌ?
Yooo-hooo!
C'è qualcuno lì? C'è qualcuno
lì? YOOO-HOOO!
Yooo-hooo!
yooo-hooo!
^D
$ killall reverb1

Si noti che il terzo grido del cliente non viene trattato fino a quando il secondo grido non si è esaurito, il
che non è molto realistico. Un'eco reale consisterebbe nella composizione delle tre grida indipendenti.
Per simularla, avremo bisogno di altre goroutine. Anche in questo caso, basta aggiungere la parola
chiave go, questa volta alla chiamata a echo:

gopl.io/ch8/reverb2
func handleConn(c net.Conn) { input :=
bufio.NewScanner(c) for
input.Scan() {
go echo(c, input.Text(), 1*time.Second)
}
// NOTA: ignorare i potenziali errori di input.Err() c.Close()
}

Gli argomenti della funzione avviata da go vengono valutati quando l'istruzione go stessa viene eseguita;
quindi input.Text() viene valutato nella goroutine principale.

Ora gli echi sono concomitanti e si sovrappongono nel tempo:

$ go build gopl.io/ch8/reverb2
$ ./reverb2 &
$ ./netcat2
C'è qualcuno lì?
C'È QUALCUNO LÌ?

www.it-ebooks.info
SEZIONE 8.4. CANALI 225

Yooo-hooo!
C'è qualcuno lì? YOOO-HOOO!
C'è qualcuno lì? Yooo-hooo!
yooo-hooo!
^D
$ killall reverb2

Per far sì che il server utilizzi la concorrenza, non solo per gestire le connessioni da più client ma anche
all'interno di una singola connessione, è bastato inserire due parole chiave go.
Tuttavia, nell'aggiungere queste parole chiave, abbiamo dovuto considerare attentamente che è sicuro
chiamare i metodi di net.Conn in modo concorrente, cosa che non è vera per la maggior parte dei tipi.
Discuteremo il concetto cruciale di sicurezza della concorrenza nel prossimo capitolo.

8.4. Canali

Se le goroutine sono le attività d i un programma Go concorrente, i canali sono le connessioni tra di


esse. Un canale è un meccanismo di comunicazione che consente a una goroutine di inviare valori a
un'altra goroutine. Ogni canale è un canale per valori di un tipo particolare, chiamato tipo di elemento
del canale. Il tipo di un canale i cui elementi sono di tipo int si scrive chan int.
Per creare un canale, si utilizza la funzione integrata make:
ch := make(chan int) // ch ha tipo 'chan int'

Come per le mappe, un canale è un riferimento alla struttura dati creata da make. Quando si copia un
canale o lo si passa come argomento a una funzione, si sta copiando un riferimento, quindi chiamante e
destinatario si riferiscono alla stessa struttura di dati. Come per altri tipi di riferimento, il valore zero di
un canale è nil.
Due canali dello stesso tipo possono essere confrontati con ==. Il confronto è vero se entrambi sono
riferimenti alla stessa struttura dati del canale. Un canale può anche essere confrontato con nil.
Un canale ha due operazioni principali, inviare e ricevere, note collettivamente come comunicazioni.
Un'istruzione send trasmette un valore da una goroutine, attraverso il canale, a un'altra goroutine che
esegue una corrispondente espressione receive. Entrambe le operazioni sono scritte con l'operatore <-.
In un'istruzione di invio, l'operatore <- separa i comandi del canale e del valore. In un'espressione di
ricezione, <- precede l'operando canale. Un'espressione di ricezione il cui risultato non viene utilizzato
è un'istruzione valida.
ch <- x // una dichiarazione di invio
x = <-ch // un'espressione di ricezione in un'istruzione di assegnazione
<-ch // un'istruzione di ricezione; il risultato viene scartato

I canali supportano una terza operazione, close, che imposta un flag che indica che non verranno più
inviati valori su questo canale; i successivi tentativi di invio andranno in panico. Le operazioni di
ricezione su un canale

www.it-ebooks.info
226 CAPITOLO 8. GOROUTINE E CANALI

Il canale chiuso restituisce i valori inviati fino a quando non ce ne sono più; qualsiasi operazione di
ricezione successiva viene completata immediatamente e restituisce il valore zero del tipo di elemento del
canale.

Per chiudere un canale, si chiama la funzione integrata close:

chiudere(ch)

Un canale creato con una semplice chiamata a make è chiamato canale non bufferizzato, ma make accetta
un secondo argomento opzionale, un intero chiamato capacità del canale. Se la capacità non è zero, make
crea un canale bufferizzato.

ch = make(chan int) // canale non bufferizzato


ch = make(chan int, 0) // canale non bufferizzato
ch = make(chan int, 3) // canale bufferizzato con capacità 3

Verranno esaminati prima i canali non bufferizzati e nella Sezione 8.4.4 i canali bufferizzati.

8.4.1. Canali non bufferizzati

Un'operazione di invio su un canale non bufferizzato blocca la goroutine di invio fino a quando un'altra
goroutine non esegue una corrispondente ricezione sullo stesso canale; a questo punto il valore viene
trasmesso ed entrambe le goroutine possono continuare. Al contrario, se l'operazione di ricezione è stata
tentata per prima, la goroutine ricevente è bloccata finché un'altra goroutine non esegue un invio sullo
stesso canale.

La comunicazione su un canale non bufferizzato causa la sincronizzazione delle goroutine di invio e


ricezione. Per questo motivo, i canali non bufferizzati sono talvolta chiamati canali sincroni. Quando un
valore viene inviato su un canale non bufferizzato, la ricezione del valore avviene prima del risveglio
della goroutine di invio.

Nelle discussioni sulla concorrenza, quando diciamo che x avviene prima di y, non intendiamo
semplicemente che x avviene prima nel tempo rispetto a y; intendiamo che è garantito che lo faccia e che
tutti i suoi effetti precedenti, come gli aggiornamenti delle variabili, siano completi e che si possa fare
affidamento su di essi.

Quando x non avviene né prima di y né dopo, si dice che x è concomitante con y. Questo non significa
che x e y siano necessariamente simultanei, ma solo che non si può dare per scontato il loro ordine.
Come vedremo nel prossimo capitolo, è necessario ordinare alcuni eventi durante l'esecuzione del
programma per evitare i problemi che sorgono quando due goroutine accedono contemporaneamente
alla stessa variabile.

Il programma client della Sezione 8.3 copia l'input al server nella sua goroutine principale, quindi il
programma client termina non appena il flusso di input si chiude, anche se la goroutine in background
sta ancora lavorando. Per far sì che il programma attenda il completamento della goroutine in
background prima di uscire, utilizziamo un canale per sincronizzare le due goroutine:

www.it-ebooks.info
SEZIONE 8.4. CANALI 227

gopl.io/ch8/netcat3
func main() {
conn, err := net.Dial("tcp", "localhost:8000") if err
:= nil {
log.Fatal(err)
}
done := make(chan struct{}) go
func() {
io.Copy(os.Stdout, conn) // NOTA: ignorare gli errori log.Println("fatto")
done <- struct{}{} // segnala la goroutine principale
}()
mustCopy(conn, os.Stdin) conn.Close()
<-fatto // aspetta che la goroutine di sfondo finisca
}

Quando l'utente chiude il flusso di input standard, mustCopy ritorna e la goroutine principale chiama
conn.Close(), chiudendo entrambe le metà della connessione di rete. La chiusura della metà in scrittura
della connessione fa sì che il server veda una condizione di fine file. La chiusura della metà in lettura fa sì
che la chiamata della goroutine principale a io.Copy restituisca un errore ''read from closed connection'',
motivo per cui abbiamo rimosso la registrazione degli errori; l'Esercizio 8.3 suggerisce una soluzione
migliore. (Si noti che l'istruzione go chiama una funzione letterale, una costruzione comune).
Prima di tornare, la goroutine di sfondo registra un messaggio e invia un valore sul canale done. La goroutine
principale attende di ricevere questo valore prima di tornare. Di conseguenza, il programma registra sempre il
messaggio "done" prima di uscire.
I messaggi inviati attraverso i canali hanno due aspetti importanti. Ogni messaggio ha un valore, ma a
volte il fatto di comunicare e il momento in cui avviene sono altrettanto importanti. Quando vogliamo
sottolineare questo aspetto, chiamiamo i messaggi eventi. Quando l'evento non trasporta informazioni
aggiuntive, cioè il suo unico scopo è la sincronizzazione, lo sottolineeremo usando un canale il cui tipo
di elemento è struct{}, anche se è comune usare un canale di bool o int per lo stesso scopo, poiché done
<- 1 è più breve di done <- struct{}{}.
Esercizio 8.3: In netcat3, il valore dell'interfaccia conn ha il tipo concreto *net.TCPConn, che
rappresenta una connessione TCP. Una connessione TCP è composta da due metà che possono
essere chiuse indipendentemente con i metodi CloseRead e CloseWrite. Modificare la goroutine
principale di netcat3 per chiudere solo la metà in scrittura della connessione, in modo che il
programma continui a stampare gli echi finali dal server reverb1 anche dopo la chiusura dello
standard input. (È più difficile fare questo per il server reverb2; si veda l'Esercizio 8.4).

8.4.2. Condotte

I canali possono essere utilizzati per collegare tra loro le goroutine, in modo che l'uscita di una sia
l'ingresso di un'altra. Questa operazione è chiamata pipeline. Il programma che segue è costituito da tre
goroutine collegate da due canali, come mostrato schematicamente nella Figura 8.1.

www.it-ebooks.info
228 CAPITOLO 8. GOROUTINE E CANALI

Figura 8.1. Una pipeline a tre stadi.


La prima goroutine, counter, genera i numeri interi 0, 1, 2, ..., e li invia su un canale alla seconda
goroutine, squarer, che riceve ogni valore, lo eleva al quadrato e invia il risultato su un altro canale alla
terza goroutine, printer, che riceve i valori al quadrato e li stampa. Per chiarezza di questo esempio,
abbiamo scelto intenzionalmente funzioni molto semplici, anche se ovviamente sono troppo banali dal
punto di vista computazionale per giustificare una propria goroutine in un programma realistico.
gopl.io/ch8/pipeline1
func main() {
naturals := make(chan int)
squares := make(chan int)

// Il contatore
va func() {
per x := 0; ; x++ { naturali
<- x
}
}()

// Squarer go
func() {
per {
x := <-naturali
quadrati <- x * x
}
}()

// Stampante (nella goroutine


principale) per {
fmt.Println(<-quadri)
}
}

Come ci si potrebbe aspettare, il programma stampa la serie infinita di quadrati 0, 1, 4, 9 e così via.
Pipeline come questa si possono trovare nei programmi server di lunga durata, dove i canali vengono
utilizzati per la comunicazione a vita tra le goroutine contenenti loop infiniti. Ma cosa succede se
vogliamo inviare solo un numero finito di valori attraverso la pipeline?
Se il mittente sa che non verranno inviati altri valori su un canale, è utile comunicare questo fatto alle
goroutine del ricevitore, in modo che possano smettere di aspettare. Questo si ottiene chiudendo il canale
con la funzione integrata close:

www.it-ebooks.info
SEZIONE 8.4. CANALI 229

close(naturals)

Dopo che un canale è stato chiuso, qualsiasi ulteriore operazione di invio su di esso andrà in panico.
Dopo che il canale chiuso è stato svuotato, cioè dopo che l'ultimo elemento inviato è stato ricevuto, tutte
le successive operazioni di ricezione procederanno senza bloccarsi, ma produrranno un valore zero. La
chiusura del canale naturale di cui sopra causerebbe la rotazione del ciclo di Squarer, che riceve un flusso
infinito di valori zero, e l'invio di questi zeri alla stampante.

Non c'è modo di verificare direttamente se un canale è stato chiuso, ma esiste una variante
dell'operazione di ricezione che produce due risultati: l'elemento del canale ricevuto e un valore
booleano, convenzionalmente chiamato ok, che è vero per una ricezione riuscita e falso per una
ricezione su un canale chiuso e prosciugato. Utilizzando questa caratteristica, possiamo modificare il
ciclo di Squarer in modo che si fermi quando il canale naturale viene svuotato e chiuda a sua volta il
canale di Squarer.
// Squarer
go func() {
per {
x, ok := <-naturali if
!ok {
break // il canale è stato chiuso e svuotato
}
quadrati <- x * x
}
chiudere(quadrati)
}()

Poiché la sintassi precedente è goffa e questo schema è comune, il linguaggio ci permette di usare un
ciclo di range anche per iterare sui canali. Questa è una sintassi più comoda per ricevere tutti i valori
inviati su un canale e terminare il ciclo dopo l'ultimo.

Nella pipeline sottostante, quando la goroutine counter termina il suo ciclo dopo 100 elementi, chiude il
canale naturals, facendo sì che la squarer termini il suo ciclo e chiuda il canale squares. (In un
programma più complesso, potrebbe avere senso che le funzioni counter e squarer rinviino le
chiamate a close all'inizio). Infine, la goroutine principale termina il suo ciclo e il programma esce.

gopl.io/ch8/pipeline2
func main() {
naturals := make(chan int)
squares := make(chan int)

// Il contatore
va func() {
per x := 0; x < 100; x++ { naturali <- x
}
close(naturals)
}()

www.it-ebooks.info
230 CAPITOLO 8. GOROUTINE E CANALI

// Squarer go
func() {
per x := range naturals { quadrati <-
x*x
}
chiudere(quadrati)
}()

// Stampante (nella goroutine


principale) per x := range squares {
fmt.Println(x)
}
}

Non è necessario chiudere ogni canale quando si è finito di usarlo. È necessario chiudere un canale
solo quando è importante comunicare alle goroutine riceventi che tutti i dati sono stati inviati. Un
canale che il garbage collector determina come irraggiungibile avrà le sue risorse recuperate
indipendentemente dal fatto che sia chiuso o meno. (Non bisogna confondere questa operazione
con quella di chiusura dei file aperti. È importante chiamare il metodo Close su ogni file quando si è
finito di usarlo).
Il tentativo di chiudere un canale già chiuso causa un panico, così come la chiusura di un canale nullo.
La chiusura dei canali ha un altro utilizzo come meccanismo di trasmissione, che verrà trattato nella
Sezione 8.9.

8.4.3. Tipi di canale unidirezionale

Quando i programmi crescono, è naturale suddividere le funzioni di grandi dimensioni in parti più
piccole. Il nostro esempio precedente utilizzava tre goroutine che comunicavano su due canali, che
erano variabili locali di main. Il programma si divide naturalmente in tre funzioni:
func counter(out chan int) func
squarer(out, in chan int) func
printer(in chan int)

La funzione squarer, posta al centro della pipeline, prende due parametri, il canale di ingresso e il
canale di uscita. Entrambi hanno lo stesso tipo, ma le loro destinazioni d'uso sono opposte: in è solo
per ricevere da, mentre out è solo per inviare a. I nomi in e out esprimono questa intenzione, ma
comunque nulla impedisce a squarer di inviare a in o ricevere da out.
Questa disposizione è tipica. Quando un canale viene fornito come parametro di una funzione, è quasi
sempre con l'intento di utilizzarlo esclusivamente per l'invio o esclusivamente per la ricezione.
Per documentare questo intento e prevenire un uso improprio, il sistema dei tipi di Go fornisce tipi di
canale unidirezionali che espongono solo una o l'altra delle operazioni di invio e ricezione. Il tipo chan<-
int, un canale di solo invio di int, consente l'invio ma non la ricezione. Al contrario, il tipo
<-chan int, un canale di sola ricezione di int, permette di ricevere ma non di inviare. (La posizione
dell'elemento
<- la freccia relativa alla parola chiave chan è un mnemonico). Le violazioni di questa disciplina
vengono rilevate in fase di compilazione.

www.it-ebooks.info
SEZIONE 8.4. CANALI 231

Poiché l'operazione di chiusura afferma che non ci saranno più invii su un canale, solo la goroutine d i
invio è in grado di chiamarla; per questo motivo è un errore di compilazione tentare di chiudere un
canale di sola ricezione.
Ecco ancora una volta la pipeline di squadratura, questa volta con tipi di canale unidirezionali:
gopl.io/ch8/pipeline3
func counter(out chan<- int) { for
x := 0; x < 100; x++ {
out <- x
}
chiudere(out)
}

func squarer(out chan<- int, in <-chan int) { for v :=


range in {
out <- v * v
}
chiudere(out)
}

func printer(in <-chan int) { for v


:= range in {
fmt.Println(v)
}
}

func main() {
naturals := make(chan int)
squares := make(chan int)

contatore (naturals)
go squarer(squares, naturals) printer(squares)
}

La chiamata counter(naturals) converte implicitamente naturals, un valore di tipo chan int, nel tipo
del parametro, chan<- int. La chiamata printer(squares) effettua una simile conversione implicita in
<-chan int. Le conversioni da tipi di canale bidirezionali a unidirezionali sono consentite in qualsiasi
assegnazione. Tuttavia, non si può tornare indietro: una volta ottenuto un valore di tipo
unidirezionale come chan<- int, non c'è modo di ottenere da esso un valore di tipo chan int che
faccia riferimento alla stessa struttura dati del canale.

8.4.4. Canali bufferizzati

Un canale bufferizzato ha una coda di elementi. La dimensione massima della coda è determinata al
momento della creazione, dall'argomento capacity di make. L'istruzione seguente crea un canale
bufferizzato in grado di contenere tre valori di stringa. La Figura 8.2 è una rappresentazione grafica di ch
e del canale a cui si riferisce.

www.it-ebooks.info
232 CAPITOLO 8. GOROUTINE E CANALI

ch = make(stringa chan, 3)

Figura 8.2. Un canale bufferato vuoto.


Un'operazione di invio su un canale bufferizzato inserisce un elemento nella parte posteriore della coda
e un'operazione di ricezione rimuove un elemento dalla parte anteriore. Se il canale è pieno, l'operazione
di invio blocca la sua goroutine finché lo spazio non viene reso disponibile da un'altra goroutine di
ricezione. Al contrario, se il canale è vuoto, un'operazione di ricezione si blocca finché non viene inviato
un valore da un'altra goroutine.
Possiamo inviare fino a tre valori su questo canale senza che la goroutine si blocchi:
ch <- "A"
ch <- "B"
ch <- "C"

A questo punto, il canale è pieno (Figura 8.3) e una quarta istruzione di invio si bloccherebbe.

Figura 8.3. Un canale a buffer completo.


Se riceviamo un valore,
fmt.Println(<-ch) // "A"

il canale non è né pieno né vuoto (Figura 8.4), quindi un'operazione di invio o di ricezione può
procedere senza bloccarsi. In questo modo, il buffer del canale disaccoppia le goroutine di invio e
ricezione.

Figura 8.4. Un canale con buffer parzialmente pieno.


Nell'improbabile caso in cui un programma abbia bisogno di conoscere la capacità del buffer del canale, è
possibile ottenerla chiamando la funzione cap incorporata:
fmt.Println(cap(ch)) // "3"

www.it-ebooks.info
SEZIONE 8.4. CANALI 233

Quando viene applicata a un canale, la funzione built-in len restituisce il numero di elementi
attualmente bufferizzati. Poiché in un programma concorrente è probabile che questa informazione sia
obsoleta non appena viene recuperata, il suo valore è limitato, ma potrebbe essere utile durante la
diagnosi dei guasti o l'ottimizzazione delle prestazioni.
fmt.Println(len(ch)) // "2"

Dopo altre due operazioni di ricezione, il canale è di nuovo vuoto e una quarta operazione si bloccherebbe:
fmt.Println(<-ch) // "B"
fmt.Println(<-ch) // "C"

In questo esempio, le operazioni di invio e ricezione sono state eseguite tutte dalla stessa goroutine, ma
nei programmi reali sono solitamente eseguite da goroutine diverse. I principianti sono talvolta tentati di
utilizzare i canali buffer all'interno di una singola goroutine come una coda, attratti dalla loro sintassi
piacevolmente semplice, ma si tratta di un errore. I canali sono profondamente legati alla
programmazione delle goroutine e, senza un'altra goroutine che riceve dal canale, un mittente - e forse
l'intero programma - rischia di rimanere bloccato per sempre. Se avete bisogno di una semplice coda,
createla utilizzando una slice.
L'esempio seguente mostra un'applicazione di un canale bufferizzato. Si tratta di una richiesta parallela a
tre mirror, cioè a server equivalenti ma geograficamente distribuiti. Invia le loro risposte su un canale
bufferizzato, quindi riceve e restituisce solo la prima risposta, che è la più veloce ad arrivare. In questo
modo mirroredQuery restituisce un risultato anche prima che i due server più lenti abbiano risposto. (Per
inciso, è abbastanza normale che diverse goroutine inviino valori allo stesso canale
contemporaneamente, come in questo esempio, o che ricevano dallo stesso canale).
func mirroredQuery() string { responses :=
make(chan string, 3)
go func() { responses <- request("asia.gopl.io") }() go func() {
responses <- request("europe.gopl.io") }()
go func() { responses <- request("americas.gopl.io") }() return <-
responses // restituisce la risposta più veloce
}
func request(hostname string) (response string) { /* ... */ }

Se avessimo usato un canale non bufferizzato, le due goroutine più lente sarebbero rimaste bloccate nel
tentativo di inviare le loro risposte su un canale dal quale nessuna goroutine riceverà mai. Questa
situazione, chiamata leak di una goroutine, sarebbe un bug. A differenza delle variabili spazzatura, le
goroutine leakate non vengono raccolte automaticamente, quindi è importante assicurarsi che le
goroutine terminino da sole quando non sono più necessarie.
La scelta tra canali unbuffered e buffered e la scelta della capacità di un canale buffered possono
entrambe influire sulla correttezza di un programma. I canali non bufferizzati offrono maggiori garanzie
di sincronizzazione perché ogni operazione di invio è sincronizzata con la relativa ricezione; con i canali
bufferizzati, queste operazioni sono disaccoppiate. Inoltre, quando si conosce un limite superiore del
numero di valori che verranno inviati su un canale, non è insolito creare un canale buffer di quella
dimensione ed eseguire tutti gli invii prima che venga ricevuto il primo valore. Se non si alloca una
capacità di buffer sufficiente, il programma si blocca.

www.it-ebooks.info
234 CAPITOLO 8. GOROUTINE E CANALI

Anche il buffering del canale può influire sulle prestazioni del programma. Immaginate tre cuochi in
una pasticceria: uno prepara la torta, l'altro la ricopre di glassa e l'altro ancora la inscrive prima di
passarla al cuoco successivo nella catena di montaggio. In una cucina con poco spazio, ogni cuoco che ha
finito una torta deve aspettare che il cuoco successivo sia pronto ad accettarla; questo rendez-vous è
analogo alla comunicazione su un canale non bufferizzato.
Se tra un cuoco e l'altro c'è spazio per una torta, un cuoco può posizionare una torta finita e iniziare
immediatamente a lavorare sulla successiva; ciò è analogo a un canale tamponato con capacità 1. Finché
i cuochi lavorano più o meno alla stessa velocità media, la maggior parte di questi passaggi procede
rapidamente, attenuando le differenze transitorie nelle rispettive velocità. Finché i cuochi lavorano in
media alla stessa velocità, la maggior parte di questi passaggi di consegne procede rapidamente,
attenuando le differenze transitorie nelle rispettive velocità. Un maggiore spazio tra i cuochi - buffer più
grandi - può attenuare le variazioni transitorie più grandi nei loro ritmi senza bloccare la catena di
montaggio, come accade quando un cuoco fa una breve pausa e poi si affretta a recuperare.
D'altra parte, se una fase precedente della catena di montaggio è costantemente più veloce di quella
successiva, il buffer tra le due passerà la maggior parte del tempo pieno. Al contrario, se la fase successiva
è più veloce, il buffer sarà solitamente vuoto. In questo caso, il buffer non offre alcun vantaggio.
La metafora della catena di montaggio è utile per i canali e le goroutine. Ad esempio, se la seconda fase è
più elaborata, un solo cuoco potrebbe non essere in grado di tenere il passo con l'offerta del primo cuoco
o di soddisfare la domanda del terzo. Per risolvere il problema, si potrebbe assumere un altro cuoco che
aiuti il secondo, svolgendo lo stesso compito ma lavorando in modo indipendente. Questo è analogo alla
creazione di un'altra goroutine che comunica sugli stessi canali.
Non abbiamo spazio per mostrarlo qui, ma il pacchetto gopl.io/ch8/cake simula questa pasticceria,
con diversi parametri che si possono variare. Include dei benchmark (§11.4) per alcuni degli scenari
descritti sopra.

8.5. Looping in Parallel

In questa sezione esploreremo alcuni modelli di concorrenza comuni per eseguire tutte le iterazioni di
un ciclo in parallelo. Considereremo il problema di produrre immagini in formato miniatura da un
insieme di immagini a grandezza naturale. Il pacchetto gopl.io/ch8/thumbnail fornisce una
funzione ImageFile in grado di scalare una singola immagine. Non ne mostreremo
l'implementazione, ma può essere scaricata da gopl.io.
gopl.io/ch8/thumbnail
miniatura del pacchetto

// ImageFile legge un'immagine da infile e scrive


// una versione in formato miniatura nella stessa directory.
// Restituisce il nome del file generato, ad esempio
"pippo.pollice.jpg". func ImageFile(infile string) (string, error)

Il programma seguente esegue un loop su un elenco di nomi di file immagine e produce una miniatura
per ciascuno di essi:

www.it-ebooks.info
SEZIONE 8.5. LOOPING IN PARALLELO 235

gopl.io/ch8/thumbnail
// makeThumbnails crea le miniature dei file specificati. func
makeThumbnails(filenames []string) {
per _, f := intervallo di nomi di file {
if _, err := thumbnail.ImageFile(f); err := nil { log.Println(err)
}
}
}

Ovviamente l'ordine di elaborazione dei file non ha importanza, poiché ogni operazione di scaling è
indipendente da tutte le altre. Problemi come questo, che consistono interamente in sottoproblemi
completamente indipendenti l'uno dall'altro, vengono definiti "imbarazzantemente paralleli". I problemi
paralleli imbarazzanti sono i più facili da implementare in modo concorrente e godono di prestazioni
che scalano linearmente con la quantità di parallelismo.

Eseguiamo tutte queste operazioni in parallelo, nascondendo così la latenza dell'I/O del file e utilizzando
più CPU per i calcoli di scalatura delle immagini. Il nostro primo tentativo di versione concorrente
aggiunge solo la parola chiave go. Per ora ignoreremo gli errori e li affronteremo in seguito.

// NOTA: non è corretto!


func makeThumbnails2(filenames []string) { for _, f
:= range filenames {
go thumbnail.ImageFile(f) // NOTA: ignorare gli errori
}
}

Questa versione viene eseguita molto velocemente, anzi troppo velocemente, poiché richiede meno
tempo dell'originale, anche quando la fetta di nomi di file contiene un solo elemento. Se non c'è
parallelismo, come può la versione concorrente essere più veloce? La risposta è che makeThumbnails
ritorna prima di aver finito di fare ciò che doveva fare. Avvia tutte le goroutine, una per nome di file, ma
non aspetta che finiscano.

Non esiste un modo diretto per aspettare che una goroutine sia terminata, ma possiamo modificare la
goroutine interna per segnalare il suo completamento alla goroutine esterna inviando un evento su un
canale condiviso. Poiché sappiamo che ci sono esattamente len(filenames) goroutine interne, la
goroutine esterna deve contare solo quel numero di eventi prima di tornare:

// makeThumbnails3 crea miniature dei file specificati in parallelo. func


makeThumbnails3(filenames []string) {
ch := make(chan struct{}) for _, f
:= range nomi file {
go func(f string) {
thumbnail.ImageFile(f) // NOTA: ignorare gli errori ch <-
struct{}{}
}(f)
}

www.it-ebooks.info
236 CAPITOLO 8. GOROUTINE E CANALI

// Attendere il completamento delle


goroutine. for range filenames {
<-ch
}
}

Si noti che abbiamo passato il valore di f come argomento esplicito alla funzione letterale, invece di
usare la dichiarazione di f dal ciclo for:
for _, f := range nomi file { go
func() {
thumbnail.ImageFile(f) // NOTA: non è corretto!
// ...
}()
}

Ricordiamo il problema della cattura di una variabile del ciclo all'interno di una funzione anonima,
descritto nella Sezione 5.6.1. Sopra, la singola variabile f è condivisa da tutti i valori della funzione
anonima e aggiornata dalle successive iterazioni del ciclo. Quando le nuove goroutine iniziano a
eseguire la funzione anonima, il ciclo for potrebbe aver aggiornato f e iniziato un'altra iterazione o
(più probabilmente) averla terminata del tutto, quindi quando queste goroutine leggono il valore di
f, osservano che ha il valore dell'elemento finale della slice. Aggiungendo un parametro esplicito, ci
assicuriamo di utilizzare il valore di f corrente al momento dell'esecuzione dell'istruzione go.
E se volessimo restituire i valori da ogni goroutine worker a quella principale? Se la chiamata a
thumbnail.ImageFile non riesce a creare un file, restituisce un errore. La versione successiva di
makeThumbnails restituisce il primo errore ricevuto da una qualsiasi delle operazioni di scalatura:
// makeThumbnails4 crea miniature per i file specificati in parallelo.
// Restituisce un errore se un passo non è riuscito.
func makeThumbnails4(filenames []string) error { errors :=
make(chan error)
for _, f := range nomi file { go
func(f string) {
_, err := thumbnail.ImageFile(f) errori
<- err
}(f)
}
per i nomi dei file dell'intervallo {
if err := <-errori; err := nil {
return err // NOTA: errata: perdita di goroutine!
}
}
restituire nil
}

Questa funzione presenta un sottile bug. Quando incontra il primo errore non nullo, restituisce l'errore
al chiamante, senza che nessuna goroutine riesca a svuotare il canale degli errori. Ogni goroutine
worker rimanente si bloccherà per sempre quando cercherà di inviare un valore su quel canale, e non
potrà mai

www.it-ebooks.info
SEZIONE 8.5. LOOPING IN PARALLELO 237

terminare. Questa situazione, una perdita di goroutine (§8.4.4), può causare il blocco dell'intero
programma o l'esaurimento della memoria.

La soluzione più semplice consiste nell'utilizzare un canale buffer con una capacità sufficiente affinché
nessuna goroutine worker si blocchi quando invia un messaggio. (Una soluzione alternativa è creare
un'altra goroutine per svuotare il canale mentre la goroutine principale restituisce il primo errore senza
ritardi).

La versione successiva di makeThumbnails utilizza un canale buffer per restituire i nomi dei file immagine
generati e gli eventuali errori.

// makeThumbnails5 crea miniature per i file specificati in parallelo.


// Restituisce i nomi dei file generati in un ordine arbitrario,
// o un errore se un passo non è riuscito.
func makeThumbnails5(filenames []string) (thumbfiles []string, err error) { type item
struct {
stringa thumbfile
err errore
}

ch := make(chan item, len(filenames)) for _,


f := range filenames {
go func(f string) { var it
item
it.thumbfile, it.err = thumbnail.ImageFile(f) ch <-
it
}(f)
}

per i nomi di file


dell'intervallo { it
:= <-ch
if it.err != nil { return
nil, it.err
}
thumbfiles = append(thumbfiles, it.thumbfile)
}

restituire thumbfiles, nil


}

La versione finale di makeThumbnails, riportata di seguito, restituisce il numero totale di byte occupati dai
nuovi file. A differenza delle versioni precedenti, però, riceve i nomi dei file non come slice ma su un
canale di stringhe, quindi non possiamo prevedere il numero di iterazioni del ciclo.

Per sapere quando è terminata l'ultima goroutine (che potrebbe non essere l'ultima a partire), dobbiamo
incrementare un contatore prima dell'avvio di ogni goroutine e decrementarlo quando ogni goroutine
finisce. Ciò richiede un tipo speciale di contatore, che possa essere manipolato in modo sicuro da più
goroutine e che fornisca un modo per aspettare che diventi zero. Questo tipo di contatore è noto come
sync.WaitGroup e il codice seguente mostra come utilizzarlo:

www.it-ebooks.info
238 CAPITOLO 8. GOROUTINE E CANALI

// makeThumbnails6 crea miniature per ogni file ricevuto dal canale.


// Restituisce il numero di byte occupati dai file creati. func
makeThumbnails6(filenames <-chan string) int64 {
dimensioni := make(chan int64)
var wg sync.WaitGroup // numero di goroutine funzionanti per f :=
range nomi file {
wg.Add(1)
// lavoratore
go func(f string) { defer
wg.Done()
thumb, err := thumbnail.ImageFile(f) if
err != nil {
log.Println(err) return
}
info, _ := os.Stat(thumb) // OK per ignorare gli errori size
<- info.Size()
}(f)
}
// più vicino
func() {
wg.Wait()
close(dimensi
oni)
}()
var totale int64
for size := range sizes { total
+= size
}
ritorno totale
}
Si noti l'asimmetria dei metodi Add e Done. Add, che incrementa il contatore, deve essere chiamato
prima dell'avvio della goroutine worker, non al suo interno; altrimenti non saremmo sicuri che Add
avvenga prima che la goroutine ''più vicina'' chiami Wait. Inoltre, Add richiede un parametro, mentre
Done no; è equivalente a Add(-1). Usiamo defer per assicurarci che il contatore venga decrementato
anche nel caso di errore. La struttura del codice qui sopra è un modello comune e idiomatico per il
looping in parallelo quando non si conosce il numero di iterazioni.
Il canale delle dimensioni riporta le dimensioni di ogni file alla goroutine principale, che le riceve
tramite un ciclo di range e calcola la somma. Osservate come viene creata una goroutine più vicina che
attende che i worker finiscano prima di chiudere il canale delle dimensioni. Queste due operazioni, wait
e close, devono essere contemporanee al ciclo sulle taglie. Considerate le alternative: se l'operazione di
attesa fosse collocata nella goroutine principale prima del ciclo, non terminerebbe mai, mentre se fosse
collocata dopo il ciclo, sarebbe irraggiungibile, poiché senza la chiusura del canale, il ciclo non
terminerebbe mai.

La Figura 8.5 illustra la sequenza di eventi della funzione makeThumbnails6 . Le linee verticali
rappresentano le goroutine. I segmenti sottili indicano il sonno, quelli spessi l'attività. Il

www.it-ebooks.info
SEZIONE 8.6. ESEMPIO: CRAWLER WEB SIMULTANEO 239

Figura 8.5. La sequenza di eventi in makeThumbnails6.

Le frecce diagonali indicano gli eventi che sincronizzano una goroutine con un'altra. Il tempo scorre
verso il basso. Si noti come la goroutine principale trascorra la maggior parte del tempo nel loop di
intervallo addormentato, in attesa che un worker invii un valore o che il closer chiuda il canale.
Esercizio 8.4: Modificare il server reverb2 in modo che utilizzi un sync.WaitGroup per ogni
connessione per contare il numero di goroutine di eco attive. Quando il valore scende a zero,
chiudere la metà di scrittura della connessione TCP come descritto nell'Esercizio 8.3. Verificate che
il client netcat3 modificato in quell'esercizio attenda gli echi finali di più grida simultanee, anche dopo
che lo standard input è stato chiuso.
Esercizio 8.5: Prendete un programma sequenziale esistente legato alla CPU, come il programma Mandelbrot
della Sezione 3.3 o il calcolo delle superfici 3D della Sezione 3.2, ed eseguite il suo ciclo principale in parallelo
utilizzando i canali di comunicazione. Quanto è più veloce l'esecuzione su una macchina multiprocessore?
Qual è il numero ottimale di goroutine da utilizzare?

8.6. Esempio: Web Crawler concorrente

Nella Sezione 5.6 abbiamo realizzato un semplice web crawler che esplora il grafo dei link del web in
ordine breadth-first. In questa sezione, lo renderemo concorrente, in modo che chiamate indipendenti a
crawl possano sfruttare il parallelismo I/O disponibile nel web. La funzione di crawl rimane esattamente
com'era in gopl.io/ch5/findlinks3:

www.it-ebooks.info
240 CAPITOLO 8. GOROUTINE E CANALI

gopl.io/ch8/crawl1
func crawl(url string) []string { fmt.Println(url)
list, err := links.Extract(url) if
err := nil {
log.Print(err)
}
restituire l'elenco
}

La funzione principale assomiglia a breadthFirst (§5.6). Come in precedenza, una lista di lavoro registra
la coda di elementi da elaborare, ogni elemento è un elenco di URL da sottoporre a crawling, ma questa
volta, invece di rappresentare la coda usando una slice, usiamo un canale. Ogni chiamata a crawl avviene
nella propria goroutine e invia i collegamenti scoperti alla lista di lavoro.

func main() {
elenco di lavoro := make(chan []stringa)

// Inizia con gli argomenti della riga di comando.


go func() { worklist <- os.Args[1:] }()

// Eseguire il crawling del web in


modo concorrente. seen :=
make(map[string]bool) for list :=
range worklist {
for _, link := range list { if
!seen[link] {
seen[link] = true
go func(link string) { worklist <-
crawl(link)
}(link)
}
}
}
}

Si noti che la goroutine crawl prende link come parametro esplicito per evitare il problema della cattura
delle variabili del ciclo che abbiamo visto per la prima volta nella Sezione 5.6.1. Si noti anche che l'invio
iniziale degli argomenti della riga di comando alla lista di lavoro deve essere eseguito nella propria
goroutine per evitare il deadlock, una situazione di blocco in cui sia la goroutine principale sia una
goroutine crawler tentano di inviare l'una all'altra mentre nessuna riceve. Una soluzione alternativa
sarebbe quella di utilizzare un canale bufferizzato.

Il crawler è ora altamente concorrente e stampa una tempesta di URL, ma ha due problemi. Il primo
problema si manifesta con messaggi di errore nel registro dopo pochi secondi di funzionamento:

$ go build gopl.io/ch8/crawl1
$ ./crawl1 http://gopl.io/
http://gopl.io/
https://golang.org/help/

www.it-ebooks.info
SEZIONE 8.6. ESEMPIO: CRAWLER WEB SIMULTANEO 241

https://golang.org/doc/ https://golang.org/blog/
...
2015/07/15 18:22:12 Get ...: dial tcp: lookup blog.golang.org: no such host 2015/07/15
18:22:12 Get ...: dial tcp 23.21.222.120:443: socket:
troppi file aperti
...

Il messaggio di errore iniziale è una sorprendente segnalazione di un errore di ricerca DNS per un
dominio affidabile. Il messaggio di errore successivo rivela la causa: il programma ha creato così tante
connessioni di rete in una volta sola che ha superato il limite per processo sul numero di file aperti,
causando il fallimento di operazioni come le ricerche DNS e le chiamate a net.Dial.

Il programma è troppo parallelo. Il parallelismo senza limiti è raramente una buona idea, poiché c'è
sempre un fattore limitante nel sistema, come il numero di core della CPU per i carichi di lavoro legati al
calcolo, il numero di spindles e heads per le operazioni di I/O su disco locale, la larghezza di banda della
rete per i download in streaming o la capacità di servizio di un servizio web. La soluzione consiste nel
limitare il numero di utilizzi paralleli della risorsa in modo che corrisponda al livello di parallelismo
disponibile. Un modo semplice per farlo, nel nostro esempio, è garantire che non siano attive più di n
chiamate a links.Extract c o n t e m p o r a n e a m e n t e , dove n è comodamente inferiore al limite del
descrittore di file, ad esempio 20. Questo è analogo al modo in cui un sistema di gestione dei file è in
grado di gestire le risorse. Questo è analogo al modo in cui un portiere di un locale notturno affollato
ammette un ospite solo quando un altro ospite se ne va.

Possiamo limitare il parallelismo utilizzando un canale buffer di capacità n per modellare un principio di
concorrenza chiamato semaforo di conteggio. Concettualmente, ciascuno degli n slot liberi nel buffer del
canale rappresenta un token che autorizza il titolare a procedere. L'invio di un valore nel canale
acquisisce un token e la ricezione di un valore dal canale rilascia un token, creando un nuovo slot libero.
In questo modo si garantisce che possano avvenire al massimo n invii senza che intervenga una
ricezione. (Anche se potrebbe essere più intuitivo trattare gli slot pieni nel buffer del canale come token,
l'uso di slot vuoti evita la necessità di riempire il buffer del canale dopo averlo creato). Poiché il tipo di
elemento del canale non è importante, useremo struct{}, che ha dimensione zero.

Riscriviamo la funzione crawl in modo che la chiamata a links.Extract sia intervallata da operazioni
di acquisizione e rilascio di un token, garantendo così che siano attive al massimo 20 chiamate in
una volta. È buona norma mantenere le operazioni di semaforo il più vicino possibile all'operazione
di I/O che regolano.
gopl.io/ch8/crawl2
// tokens è un semaforo di conteggio usato per
// imporre un limite di 20 richieste contemporanee.
var tokens = make(chan struct{}, 20)

func crawl(url string) []string { fmt.Println(url)


tokens <- struct{}{} // acquisisce un
elenco di token, err := links.Extract(url)
<-token // rilascia il token

www.it-ebooks.info
242 CAPITOLO 8. GOROUTINE E CANALI

if err != nil { log.Print(err)


}
restituire l'elenco
}

Il secondo problema è che il programma non termina mai, anche quando ha scoperto tutti i
collegamenti raggiungibili dagli URL iniziali. (Naturalmente, è improbabile che si noti questo problema,
a meno che non si scelgano con cura gli URL iniziali o si implementi la funzione di limitazione della
profondità dell'Esercizio 8.6). Affinché il programma termini, dobbiamo uscire dal ciclo principale
quando la lista di lavoro è vuota e non ci sono goroutine di crawling attive.
func main() {
elenco di lavoro := make(chan []string)
var n int // numero di invii in sospeso nella lista di lavoro

// Iniziare con gli argomenti della riga di


comando. n++
go func() { worklist <- os.Args[1:] }()

// Eseguire il crawling del web in


modo concorrente. seen :=
make(map[string]bool) for ; n > 0; n--
{
elenco := <elenco di lavoro
for _, link := range list { if
!seen[link] {
seen[link] = true n++
go func(link string) { worklist <-
crawl(link)
}(link)
}
}
}
}

In questa versione, il contatore n tiene traccia del numero di invii alla lista di lavoro che devono ancora
avvenire. Ogni volta che sappiamo che un elemento deve essere inviato alla lista di lavoro,
incrementiamo n, una volta prima di inviare gli argomenti iniziali della riga di comando e un'altra volta
ogni volta che avviamo una goroutine di crawler. Il ciclo principale termina quando n scende a zero,
poiché non c'è più lavoro da fare.

Ora il crawler concorrente viene eseguito circa 20 volte più velocemente del crawler breadth-first della
Sezione 5.6, senza errori, e termina correttamente se deve completare il suo compito.

Il programma seguente mostra una soluzione alternativa al problema dell'eccessiva concomitanza.


Questa versione utilizza la funzione crawl originale che non ha un semaforo di conteggio, ma la
chiama da una delle 20 goroutine crawler a lunga durata, assicurando così che siano attive al massimo
20 richieste HTTP in contemporanea.

www.it-ebooks.info
SEZIONE 8.6. ESEMPIO: CRAWLER WEB SIMULTANEO 243

gopl.io/ch8/crawl3
func main() {
worklist := make(chan []string) // liste di URL, che possono avere duplicati unseenLinks
:= make(chan string) // URL de-duplicati
// Aggiungere gli argomenti della riga di comando
alla lista di lavoro. go func() { lista di lavoro <-
os.Args[1:] }()
// Creare 20 goroutine crawler per recuperare ogni link non visto. for
i := 0; i < 20; i++ {
go func() {
for link := range unseenLinks {
foundLinks := crawl(link)
go func() { worklist <- foundLinks }()
}
}()
}
// La goroutine principale elimina gli elementi della lista di lavoro.
// e invia quelli non visti ai crawler. seen :=
make(map[string]bool)
for list := range worklist { for _,
link := range list {
if !seen[link] { seen[link] =
true unseenLinks <-
link
}
}
}
}

Le goroutine crawler sono tutte alimentate dallo stesso canale, unseenLinks. La goroutine principale è
responsabile della de-duplicazione degli elementi che riceve dall'elenco di lavoro e dell'invio di ogni
elemento non visto attraverso il canale unseenLinks a una goroutine crawler.
La mappa vista è confinata all'interno della goroutine principale, cioè vi si può accedere solo da quella
goroutine. Come altre forme di occultamento delle informazioni, il confinamento ci aiuta a ragionare
sulla correttezza di un programma. Ad esempio, le variabili locali non possono essere citate per nome al
di fuori della funzione in cui sono dichiarate; le variabili che non sfuggono (§2.3.4) a una funzione non
possono essere accessibili al di fuori di tale funzione; e i campi incapsulati di un oggetto non possono
essere accessibili se non dai metodi di tale oggetto. In tutti i casi, l'occultamento delle informazioni aiuta
a limitare le interazioni indesiderate tra le parti del programma.
I collegamenti trovati dal crawl vengono inviati alla lista di lavoro da una goroutine dedicata per
evitare deadlock. Per risparmiare spazio, in questo esempio non abbiamo affrontato il problema
della terminazione.
Esercizio 8.6: Aggiungere la limitazione della profondità al crawler concorrente. Se l'utente imposta -
depth=3, verranno recuperati solo gli URL raggiungibili da un massimo di tre link.
Esercizio 8.7: Scrivere un programma concorrente che crea un mirror locale di un sito web, recuperando
ogni pagina raggiungibile e scrivendola in una directory sul disco locale. Solo le pagine all'interno della
directory

www.it-ebooks.info
244 CAPITOLO 8. GOROUTINE E CANALI

Il dominio originale (ad esempio, golang.org) deve essere recuperato. Gli URL all'interno delle
pagine specchiate devono essere modificati, se necessario, in modo che facciano riferimento alla
pagina specchiata e non all'originale.

8.7. Multiplexing con selezione

Il programma seguente esegue il conto alla rovescia per il lancio di un razzo. La funzione time.Tick
restituisce un canale su cui invia periodicamente eventi, agendo come un metronomo. Il valore di ogni
evento è un timestamp, ma raramente è interessante quanto il fatto che sia stato inviato.
gopl.io/ch8/countdown1
func main() {
fmt.Println("Inizia il conto alla rovescia.") tick :=
time.Tick(1 * time.Second)
for countdown := 10; countdown > 0; countdown-- {
fmt.Println(countdown)
<-tick
}
lancio()
}

Ora aggiungiamo la possibilità di interrompere la sequenza di lancio premendo il tasto return durante il
conto alla rovescia. Per prima cosa, avviamo una goroutine che tenta di leggere un singolo byte dallo
standard input e, se ci riesce, invia un valore su un canale chiamato abort.
gopl.io/ch8/countdown2
abort := make(chan struct{}) go
func() {
os.Stdin.Read(make([]byte, 1)) // legge un singolo byte abort <-
struct{}{}
}()

Ora ogni iterazione del ciclo del conto alla rovescia deve attendere l'arrivo di un evento su uno dei due
canali: il canale del ticker se tutto è a posto (''nominale'' nel gergo della NASA) o un evento di
interruzione se c'è stata un''anomalia''. Non possiamo semplicemente ricevere da ogni canale perché
qualsiasi operazione tentiamo per prima si bloccherà fino al completamento. Abbiamo bisogno di
moltiplicare queste operazioni e per farlo abbiamo bisogno di un'istruzione select.
select { case
<-ch1:
// ...
caso x := <-ch2:
// ... usare x...
caso ch3 <- y:
// ...
predefinito:
// ...
}

www.it-ebooks.info
SEZIONE 8.7. MULTIPLEXING CON SELEZIONE 245

La forma generale di un'istruzione select è mostrata sopra. Come un'istruzione switch, ha un numero
di casi e un default opzionale. Ogni caso specifica una comunicazione (un'operazione di invio o
ricezione su qualche canale) e un blocco di istruzioni associato. L'espressione receive può apparire da
sola, come nel primo caso, o all'interno di una breve dichiarazione di variabile, come nel secondo
caso; la seconda forma consente di fare riferimento al valore ricevuto.
Una select attende che una comunicazione per un caso sia pronta a procedere. Esegue quindi la
comunicazione ed esegue le istruzioni associate al caso; le altre comunicazioni non avvengono. Una
select senza casi, select{}, attende per sempre.

Torniamo al nostro programma di lancio del razzo. La funzione time.After restituisce immediatamente
un canale e avvia una nuova goroutine che invia un singolo valore su quel canale dopo il tempo
specificato. L'istruzione select sottostante attende l'arrivo del primo di due eventi: un evento di
interruzione o l'evento che indica che sono trascorsi 10 secondi. Se passano 10 secondi e non si verifica
alcuna interruzione, il lancio procede.
func main() {
// ... creare un canale di interruzione...

fmt.Println("Inizia il conto alla rovescia. Premere return per


interromperlo.") select {
case <-time.After(10 * time.Second):
// Non fare nulla.
caso <-abort:
fmt.Println("Lancio interrotto!")
return
}
lancio()
}

L'esempio seguente è più sottile. Il canale ch, la cui dimensione del buffer è 1, è alternativamente vuoto e
pieno, quindi solo uno dei casi può procedere, o l'invio quando i è pari, o la ricezione quando i è
dispari. Viene sempre stampato 0 2 4 6 8.
ch := make(chan int, 1) for i
:= 0; i < 10; i++ {
selezionare {
caso x := <-ch:
fmt.Println(x) // "0" "2" "4" "6" "8" case
ch <- i:
}
}

Se sono pronti più casi, select ne sceglie uno a caso, garantendo che ogni canale abbia le stesse possibilità
di essere selezionato. Aumentando la dimensione del buffer dell'esempio precedente, l'uscita non è
deterministica, perché quando il buffer non è né pieno né vuoto, lo stato select lancia figurativamente
una moneta.
Facciamo in modo che il nostro programma di lancio stampi il conto alla rovescia. L'istruzione select qui
sotto fa sì che ogni iterazione del ciclo attenda fino a 1 secondo per un'interruzione, ma non di più.

www.it-ebooks.info
246 CAPITOLO 8. GOROUTINE E CANALI

gopl.io/ch8/countdown3
func main() {
// ... creare un canale di interruzione...
fmt.Println("Inizia il conto alla rovescia. Premere return per
interromperlo") tick := time.Tick(1 * time.Second)
for countdown := 10; countdown > 0; countdown-- {
fmt.Println(countdown)
select { case
<-tick:
// Non fare nulla.
caso <-abort:
fmt.Println("Lancio interrotto!") return
}
}
lancio()
}

La funzione time.Tick si comporta come se creasse una goroutine che chiama time.Sleep in un ciclo,
inviando un evento ogni volta che si sveglia. Quando la funzione conto alla rovescia ritorna, smette di
ricevere eventi da tick, ma la goroutine ticker è ancora lì, cercando invano di inviare su un canale da cui
nessuna goroutine sta ricevendo - una perdita di goroutine (§8.4.4).
La funzione Tick è comoda, ma è appropriata solo quando i tick saranno necessari per tutta la durata
dell'applicazione. Altrimenti, si dovrebbe usare questo schema:
ticker := time.NewTicker(1 * time.Second)
<-ticker.C // riceve dal canale del ticker ticker.Stop() // fa terminare
la goroutine del ticker

A volte si vuole provare a inviare o ricevere su un canale, evitando però di bloccarsi se il canale non è
pronto: una comunicazione non bloccante. Un'istruzione select può fare anche questo. Una select può
avere un default, che specifica cosa fare quando nessuna delle altre comunicazioni può procedere
immediatamente.
L'istruzione select qui sotto riceve un valore dal canale di interruzione, se ce n'è uno da ricevere;
altrimenti non fa nulla. Si tratta di un'operazione di ricezione non bloccante; eseguirla
ripetutamente si chiama polling di un canale.
selezionare {
caso <-aborto:
fmt.Printf("Lancio interrotto!\n") return
predefinito:
// non fare nulla
}

Il valore zero per un canale è nil. Forse sorprendentemente, i canali nulli sono talvolta utili. Poiché le
operazioni di invio e ricezione su un canale nullo si bloccano per sempre, un caso in un'istruzione select

www.it-ebooks.info
SEZIONE 8.8. ESEMPIO: ATTRAVERSAMENTO SIMULTANEO DI DIRECTORY 247

il cui canale è nil non viene mai selezionato. Questo ci permette di usare nil per abilitare o disabilitare
casi che rispondono a funzioni come la gestione di timeout o cancellazioni, la risposta ad altri eventi di
input o l'emissione di output. Vedremo un esempio nella prossima sezione.
Esercizio 8.8: Utilizzando un'istruzione select, aggiungete un timeout al server echo della Sezione 8.3 in
modo che disconnetta qualsiasi client che non grida nulla entro 10 secondi.

8.8. Esempio: Attraversamento concorrente della directory

In questa sezione, costruiremo un programma che riporta l'utilizzo del disco di una o più directory
specificate sulla riga di comando, come il comando Unix du. La maggior parte del lavoro è svolta
dalla funzione walkDir, che enumera le voci della directory dir utilizzando la funzione helper dirents.
gopl.io/ch8/du1
// walkDir percorre ricorsivamente l'albero dei file con radice in dir
// e invia la dimensione di ogni file trovato su fileSizes. func
walkDir(dir string, fileSizes chan<- int64) {
for _, entry := range dirents(dir) { if
entry.IsDir() {
subdir := filepath.Join(dir, entry.Name())
walkDir(subdir, fileSizes)
} else {
fileSizes <- entry.Size()
}
}
}

// dirents restituisce le voci della directory dir. func


dirents(dir string) []os.FileInfo {
entries, err := ioutil.ReadDir(dir) if
err != nil {
fmt.Fprintf(os.Stderr, "du1: %v\n", err) return nil
}
restituire le voci
}

La funzione ioutil.ReadDir restituisce una fetta di os.FileInfo, le stesse informazioni che una
chiamata a os.Stat restituisce per un singolo file. Per ogni sottodirectory, walkDir richiama
ricorsivamente se stesso e per ogni file, walkDir invia un messaggio sul canale fileSizes. Il messaggio è
la dimensione del file in byte.
La funzione principale, mostrata di seguito, utilizza due goroutine. La goroutine di sfondo chiama
walkDir per ogni directory specificata sulla riga di comando e infine chiude il canale fileSizes. La
goroutine principale calcola la somma delle dimensioni dei file che riceve dal canale e infine stampa
il totale.

www.it-ebooks.info
248 CAPITOLO 8. GOROUTINE E CANALI

// Il comando du1 calcola l'utilizzo del disco dei file in una directory. pacchetto main
importare (
"bandiera"
"fmt" "io/ioutil"
" os"
"path/filepath"
)
func main() {
// Determinare le directory iniziali.
flag.Parse()
roots := flag.Args() if
len(roots) == 0 {
radici = []string{"."}
}
// Attraversare l'albero dei file.
fileSizes := make(chan int64) go
func() {
per _, root := range roots { walkDir(root,
fileSizes)
}
chiudere(fileDimensioni)
}()
// Stampa i risultati. var
nfiles, nbytes int64
per dimensione := intervallo
fileDimensioni { nfiles++
nbyte += dimensione
}
printDiskUsage(nfiles, nbytes)
}
func printDiskUsage(nfiles, nbytes int64) {
fmt.Printf("%d file %.1f GB\n", nfiles, float64(nbytes)/1e9)
}

Questo programma si ferma a lungo prima di stampare il risultato:


$ go build gopl.io/ch8/du1
$ ./du1 $HOME /usr /bin /etc
213201 file 62,7 GB

Il programma sarebbe più bello se ci tenesse informati sui suoi progressi. Tuttavia, spostando
semplicemente la chiamata a printDiskUsage nel ciclo, il programma stamperebbe migliaia di righe di
output.
La variante di du sotto riportata stampa i totali periodicamente, ma solo se viene specificato il flag -v,
poiché non tutti gli utenti vorranno vedere i messaggi di avanzamento. La goroutine di sfondo che
esegue il loop sulle radici rimane invariata. La goroutine principale ora utilizza un ticker per generare
eventi ogni

www.it-ebooks.info
SEZIONE 8.8. ESEMPIO: ATTRAVERSAMENTO SIMULTANEO DI DIRECTORY 249

500 ms e un'istruzione select per attendere un messaggio sulla dimensione del file, nel qual caso aggiorna
i totali, o un evento tick, nel qual caso stampa i totali correnti. Se il flag -v non è specificato, il
canale tick rimane nullo e il suo caso nella select è effettivamente disabilitato.
gopl.io/ch8/du2
var verbose = flag.Bool("v", false, "mostra messaggi di avanzamento verbosi")
func main() {
// ...avviare la goroutine di sfondo...
// Stampa periodicamente i risultati.
var tick <-chan time.Time
if *verbose {
tick = time.Tick(500 * time.Millisecond)
}
var nfiles, nbytes int64 loop:
for {
selezion
are {
case size, ok := <-fileSizes: if
!ok {
break loop // fileSizes è stato chiuso
}
nfiles++ nbytes
+= dimensione
caso <-tick:
printDiskUsage(nfiles, nbytes)
}
}
printDiskUsage(nfiles, nbytes) // totali finali
}

Poiché il programma non utilizza più un ciclo di range, il primo caso select deve verificare
esplicitamente se il canale fileSizes è stato chiuso, utilizzando la forma a due risultati
dell'operazione receive. Se il canale è stato chiuso, il programma esce dal ciclo. L'istruzione di
interruzione etichettata interrompe sia la select che il ciclo for; un'interruzione non etichettata
interromperebbe solo la select, facendo iniziare il ciclo all'iterazione successiva.
Il programma ci offre ora un piacevole flusso di aggiornamenti:
$ go build gopl.io/ch8/du2
$ ./du2 -v $HOME /usr /bin /etc 28608 file
8,3 GB
54147 file 10,3 GB
93591 file 15,1 GB
127169 file 52,9 GB
175931 file 62,2 GB
213201 file 62,7 GB

Tuttavia, ci vuole ancora troppo tempo per finire. Non c'è motivo per cui tutte le chiamate a walkDir
non possano essere eseguite in modo simultaneo, sfruttando così il parallelismo nel sistema del disco. La
terza versione di du,

www.it-ebooks.info
250 CAPITOLO 8. GOROUTINE E CANALI

utilizza un sync.WaitGroup (§8.5) per contare il numero di chiamate a walkDir ancora attive e una
goroutine più vicina per chiudere il canale fileSizes quando il contatore scende a zero.
gopl.io/ch8/du3
func main() {
// ...determinare le radici...

// Attraversare ogni radice dell'albero dei file in parallelo.


fileSizes := make(chan int64)
var n sync.WaitGroup
per _, root := range roots { n.Add(1)
go walkDir(root, &n, fileSizes)
}
go func() {
n.Wait() close(fileSizes)
}()
// ...selezionare il ciclo...
}

func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) { defer


n.Done()
for _, entry := range dirents(dir) { if
entry.IsDir() {
n.Add(1)
subdir := filepath.Join(dir, entry.Name()) go
walkDir(subdir, n, fileSizes)
} else {
fileSizes <- entry.Size()
}
}
}

Poiché questo programma crea molte migliaia di goroutine al suo apice, dobbiamo cambiare i direnti
per usare un semaforo di conteggio per evitare che apra troppi file in una volta sola, proprio come
abbiamo fatto per il web crawler nella Sezione 8.6:
// sema è un semaforo di conteggio per limitare la concorrenza nelle direnti. var
sema = make(chan struct{}, 20)

// dirents restituisce le voci della directory dir. func


dirents(dir string) []os.FileInfo {
sema <- struct{}{} // acquisire il
token defer func() { <-sema }() // rilasciare il
token
// ...

Questa versione funziona diverse volte più velocemente della precedente, anche se c'è molta variabilità
da sistema a sistema.

www.it-ebooks.info
SEZIONE 8.9. ANNULLAMENTO 251

Esercizio 8.9: Scrivere una versione di du che calcoli e visualizzi periodicamente totali separati per
ciascuna delle directory principali.

8.9. Cancellazione

A volte è necessario indicare a una goroutine di interrompere la sua attività, ad esempio in un server
web che esegue un calcolo per conto di un cliente che si è disconnesso.
Non c'è modo per una goroutine di terminare direttamente un'altra, poiché ciò lascerebbe tutte le sue
variabili condivise in stati non definiti. Nel programma di lancio del razzo (§8.7) abbiamo inviato un
singolo valore su un canale chiamato abort, che la goroutine del conto alla rovescia ha interpretato come
una richiesta di arresto. Ma cosa succede se dobbiamo annullare due goroutine, o un numero arbitrario?
Una possibilità potrebbe essere quella di inviare tanti eventi sul canale abort quante sono le goroutine da
annullare. Tuttavia, se alcune goroutine sono già terminate da sole, il conteggio sarà troppo grande e
l'invio si bloccherà. D'altra parte, se queste goroutine hanno generato altre goroutine, il conteggio sarà
troppo piccolo e alcune goroutine rimarranno ignare della cancellazione. In generale, è difficile sapere
quante goroutine stanno lavorando per nostro conto in un dato momento. Inoltre, quando una
goroutine riceve un valore dal canale di interruzione, lo consuma in modo che le altre goroutine non lo
vedano. Per la cancellazione, abbiamo bisogno di un meccanismo affidabile per trasmettere un evento su
un canale, in modo che molte goroutine lo vedano mentre si verifica e possano poi constatare che si è
verificato.
Ricordiamo che dopo che un canale è stato chiuso e svuotato di tutti i valori inviati, le successive
operazioni di ricezione procedono immediatamente, producendo valori nulli. Si può sfruttare questo
aspetto per creare un meccanismo di tipo "broad cast": se non si invia un valore sul canale, lo si chiude.
Possiamo aggiungere la cancellazione al programma du della sezione precedente con alcune
semplici modifiche. Innanzitutto, creiamo un canale di cancellazione sul quale non viene mai
inviato alcun valore, ma la cui chiusura indica che è giunto il momento che il programma
interrompa la sua attività. Definiamo anche una funzione di utilità, cancelled, che controlla o
interroga lo stato di cancellazione nel momento in cui viene chiamata.
gopl.io/ch8/du4
var done = make(chan struct{})
func cancelled() bool {
select {
caso <-fatto:
restituire true
predefinito:
restituire false
}
}

Successivamente, si crea una goroutine che leggerà dallo standard input, che di solito è collegato al
terminale. Non appena viene letto un input (ad esempio, l'utente preme il tasto return), questa goroutine
trasmette la cancellazione chiudendo il canale done.

www.it-ebooks.info
252 CAPITOLO 8. GOROUTINE E CANALI

// Annulla la traversata quando viene rilevato un


input. go func() {
os.Stdin.Read(make([]byte, 1)) // legge un singolo byte
close(done)
}()

Ora dobbiamo fare in modo che le nostre goroutine rispondano alla cancellazione. Nella goroutine
principale, aggiungiamo un terzo caso all'istruzione select che cerca di ricevere dal canale done. La
funzione ritorna se questo caso viene selezionato, ma prima di ritornare deve prima svuotare il
canale fileSizes, scartando tutti i valori finché il canale non viene chiuso. Questo per garantire che
qualsiasi chiamata attiva a walkDir possa essere eseguita fino al completamento senza rimanere
bloccata nell'invio a fileSizes.
for {
selezion
are {
caso <-fatto:
// Scarico di fileSizes per consentire alle goroutine esistenti di
terminare. for range fileSizes {
// Non fare nulla.
}
ritorno
case size, ok := <-fileSizes:
// ...

}
}

La goroutine walkDir controlla lo stato di cancellazione quando inizia e ritorna senza fare nulla se lo stato è
impostato. Questo trasforma tutte le goroutine create dopo la cancellazione in no-op:
func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) { defer
n.Done()
se annullato() {
ritorno
}
per _, entry := range dirents(dir) {
// ...
}
}

Potrebbe essere utile interrogare nuovamente lo stato di cancellazione all'interno del ciclo di walkDir, per
evitare di creare goroutine dopo l'evento di cancellazione. La cancellazione comporta un compromesso:
una risposta più rapida spesso richiede modifiche più invasive alla logica del programma. Garantire che
non si verifichino operazioni costose dopo l'evento di cancellazione può richiedere l'aggiornamento di
molti punti del codice, ma spesso la maggior parte dei vantaggi si ottiene controllando la cancellazione
in pochi punti importanti.

Un po' di profiling di questo programma ha rivelato che il collo di bottiglia era l'acquisizione di un token
semaforico in dirents. La selezione seguente rende questa operazione annullabile e riduce la latenza di
annullamento tipica del programma da centinaia di millisecondi a decine:

www.it-ebooks.info
SEZIONE 8.10. ESEMPIO: SERVER CHAT 253

func dirents(dir string) []os.FileInfo { select {


caso sema <- struct{}{}: // acquisire token caso
<-fatto:
return nil // annullato
}
defer func() { <-sema }() // rilascia il token

// ...leggi directory...

Ora, quando si verifica la cancellazione, tutte le goroutine in background si fermano rapidamente e la


funzione principale ritorna. Naturalmente, quando main ritorna, il programma esce, quindi può essere
difficile distinguere una funzione main che si ripulisce da sola da una che non lo fa. C'è un trucco utile
che possiamo usare durante i test: se invece di tornare da main in caso di cancellazione, eseguiamo una
chiamata a panic, il runtime eseguirà il dump dello stack di ogni goroutine del programma. Se la
goroutine main è l'unica rimasta, allora ha fatto piazza pulita. Se invece rimangono altre goroutine, è
possibile che non siano state cancellate correttamente, oppure che siano state cancellate ma che la
cancellazione richieda del tempo; può valere la pena di fare qualche indagine. Il panic dump spesso
contiene informazioni sufficienti per distinguere questi casi.

Esercizio 8.10: Le richieste HTTP possono essere annullate chiudendo il canale opzionale Cancel nel file
http.Request struct. Modificare il web crawler della Sezione 8.6 per supportare la cancellazione.

Suggerimento: la funzione http.Get non consente di personalizzare una richiesta. Si deve invece creare
la richiesta usando http.NewRequest, impostare il campo Cancel e quindi formulare la richiesta
chiamando http.DefaultClient.Do(req).

Esercizio 8.11: Seguendo l'approccio di mirroredQuery nella Sezione 8.4.4, implementare una variante di
fetch che richieda più URL contemporaneamente. Non appena arriva la prima risposta, annullare le
altre richieste.

8.10. Esempio: Chat Server

Termineremo questo capitolo con un server di chat che consente a diversi utenti di trasmettere messaggi
testuali tra loro. In questo programma ci sono quattro tipi di goroutine. C'è un'istanza a testa delle
goroutine main e broadcaster, mentre per ogni connessione client c'è una goroutine handle Conn e
una clientWriter. La goroutine broadcaster illustra bene l'uso di select, poiché deve rispondere a tre
diversi tipi di messaggi.

Il compito della goroutine principale, mostrata di seguito, è quello di ascoltare e accettare le connessioni
di rete in arrivo dai client. Per ognuna di esse, crea una nuova goroutine handleConn, proprio come nel
server di eco corrente visto all'inizio di questo capitolo.

www.it-ebooks.info
254 CAPITOLO 8. GOROUTINE E CANALI

gopl.io/ch8/chat
func main() {
listener, err := net.Listen("tcp", "localhost:8000") if err :=
nil {
log.Fatal(err)
}
go broadcaster()
for {
conn, err := listener.Accept() if
err := nil {
log.Print(err)
continua
}
vai a gestireConn(conn)
}
}

Il prossimo è l'emittente. La sua variabile locale clients registra l'insieme attuale dei client connessi.
L'unica informazione registrata su ogni client è l'identità del suo canale di messaggi in uscita, di cui si
parlerà più avanti.
tipo client chan<- string // un canale di messaggi in uscita var (
entrare = make(chan client)
leaving = make(chan client)
messaggi = make(chan string) // tutti i messaggi client in arrivo
)
func broadcaster() {
client := make(map[client]bool) // tutti i client connessi per {
selezionare {
caso msg := <-messaggi:
// Trasmette il messaggio in arrivo a tutti
// i canali dei messaggi in uscita dei client.
for cli := range clients {
cli <- msg
}
case cli := <-entry:
clients[cli] = true
case cli := <lasciando:
delete(clients, cli) close(cli)
}
}
}

L'emittente ascolta sui canali globali di entrata e uscita gli annunci dei clienti in arrivo e in
partenza. Quando riceve uno di questi eventi, aggiorna i client

www.it-ebooks.info
SEZIONE 8.10. ESEMPIO: SERVER CHAT 255

e, se l'evento era una partenza, chiude il canale dei messaggi in uscita del client. L'emittente ascolta anche
gli eventi sul canale dei messaggi globali, al quale ogni client invia tutti i suoi messaggi in entrata. Quando
l'emittente riceve uno di questi eventi, trasmette il messaggio a tutti i client connessi.

Vediamo ora le goroutine per cliente. La funzione handleConn crea un nuovo canale di messaggi in uscita
per il proprio client e annuncia l'arrivo di questo client all'emittente attraverso il canale in entrata. Quindi
legge ogni riga di testo dal client, inviando ogni riga all'emittente attraverso il canale globale dei
messaggi in entrata, anteponendo a ogni messaggio l'identità del mittente. Quando non c'è più nulla da
leggere dal client, handleConn annuncia la partenza del client sul canale di uscita e chiude la connessione.

func handleConn(conn net.Conn) {


ch := make(chan string) // i messaggi client in uscita vanno
clientWriter(conn, ch)

who := conn.RemoteAddr().String() ch <-


"Tu sei " + who
messaggi <- chi + " è arrivato" entrare <- ch

input := bufio.NewScanner(conn) for


input.Scan() {
messaggi <- chi + ": " + input.Text()
}
// NOTA: ignorare i potenziali errori di input.Err()

lasciando <- ch
messaggi <- chi + " ha lasciato"
conn.Close()
}

func clientWriter(conn net.Conn, ch <-chan string) { for msg


:= range ch {
fmt.Fprintln(conn, msg) // NOTA: ignorare gli errori di rete
}
}

Inoltre, handleConn crea una goroutine clientWriter per ogni client che riceve i messaggi trasmessi al
canale dei messaggi in uscita del client e li scrive sulla connessione di rete del client. Il ciclo del client
writer termina quando l'emittente chiude il canale dopo aver ricevuto una notifica di abbandono.

La schermata seguente mostra il server in azione con due client in finestre separate sullo stesso
computer, utilizzando netcat per la chat:

$ go build gopl.io/ch8/chat
$ go build gopl.io/ch8/netcat3

www.it-ebooks.info
256 CAPITOLO 8. GOROUTINE E CANALI

$ ./chat &
$ ./netcat3
Sei 127.0.0.1:64208 $ ./netcat3 127.0.0.1:64211
è arrivato Sei 127.0.0.1:64211 Ciao!
127.0.0.1:64208: Ciao! 127.0.0.1:64208: Ciao!
Ciao a te stesso.
127.0.0.1:64211: Ciao a te stesso. 127.0.0.1:64211: Ciao a te stesso.
^C
127.0.0.1:64208 è partito
$ ./netcat3
Siete 127.0.0.1:64216 127.0.0.1:64216 è arrivato
Benvenuto.
127.0.0.1:64211: Benvenuto. 127.0.0.1:64211: Benvenuto.
^C
127.0.0.1:64211 se n'è andato

Durante l'hosting di una sessione di chat per n client, questo programma esegue 2n+2 goroutine
comunicanti in modo concorrente, ma non necessita di operazioni di blocco esplicite (§9.2). La mappa
dei client è collegata a una sola goroutine, l'emittente, quindi non è possibile accedervi in modo
concorrente. Le uniche variabili condivise da più goroutine sono i canali e le istanze di net.Conn,
entrambe sicure per la concorrenza. Parleremo ancora di confinamento, sicurezza della concorrenza e
delle implicazioni della condivisione di variabili tra goroutine nel prossimo capitolo.
Esercizio 8.12: Fare in modo che l'emittente annunci l'attuale insieme di clienti a ogni nuovo
arrivo. A tal fine, è necessario che l'insieme dei clienti e i canali in entrata e in uscita registrino
anche il nome del cliente.
Esercizio 8.13: Fare in modo che il server di chat disconnetta i client inattivi, come quelli che non hanno
inviato messaggi negli ultimi cinque minuti. Suggerimento: la chiamata a conn.Close() in un'altra
goroutine sblocca le chiamate di lettura attive, come quella effettuata da input.Scan().
Esercizio 8.14: Modificare il protocollo di rete del server di chat in modo che ogni client fornisca il
proprio nome all'ingresso. Usare questo nome invece dell'indirizzo di rete quando si antepone a ogni
messaggio l'identità del mittente.
Esercizio 8.15: Se un programma client non riesce a leggere i dati in modo tempestivo, tutti i client si
bloccano. Modificate l'emittente in modo che salti un messaggio piuttosto che aspettare se un client
writer non è pronto ad accettarlo. In alternativa, aggiungete del buffering al canale dei messaggi in uscita
di ogni client, in modo che la maggior parte dei messaggi non venga abbandonata; l'emittente dovrebbe
utilizzare un invio non bloccante a questo canale.

www.it-ebooks.info
9
Concorrenza con
variabili condivise

Nel capitolo precedente abbiamo presentato diversi programmi che utilizzano goroutine e canali per
esprimere la concorrenza in m o d o diretto e naturale. Tuttavia, nel f a r l o , abbiamo sorvolato su una
serie di questioni importanti e sottili che i programmatori devono tenere a mente quando scrivono codice
concorrente.
In questo capitolo esamineremo più da vicino la meccanica della concorrenza. In particolare, verranno
evidenziati alcuni dei problemi associati alla condivisione di variabili tra più goroutine, le tecniche
analitiche per riconoscere tali problemi e i modelli per risolverli. Infine, spiegheremo alcune differenze
tecniche tra le goroutine e i thread del sistema operativo.

9.1. Gara Condizioni

In un programma sequenziale, cioè un programma con una sola goroutine, i passi del programma
avvengono nel noto ordine di esecuzione determinato dalla logica del programma. Ad esempio, in una
sequenza di istruzioni, la prima avviene prima della seconda e così via. In un programma con due o più
goroutine, i passi all'interno di ciascuna goroutine avvengono nell'ordine noto, ma in generale non
sappiamo se un evento x in una goroutine avviene prima di un evento y in un'altra goroutine, o se avviene
dopo, o se è simultaneo. Quando non possiamo dire con certezza che un evento accade prima dell'altro,
allora gli eventi x e y sono simultanei.
Consideriamo una funzione che funziona correttamente in un programma sequenziale. Tale funzione è
concur- rency-safe se continua a funzionare correttamente anche quando viene chiamata in modo
concorrente, cioè da due o più goroutine senza alcuna sincronizzazione aggiuntiva. Si può generalizzare
questa nozione a un insieme di

257

www.it-ebooks.info
258 CAPITOLO 9. CONCORRENZA CON VARIABILI CONDIVISE

funzioni collaboranti, come i metodi e le operazioni di un particolare tipo. Un tipo è sicuro dal punto di
vista della concorrenza se tutti i suoi metodi e operazioni accessibili sono sicuri dal punto di vista della
concorrenza.
È possibile rendere un programma sicuro dal punto di vista della concorrenza senza rendere sicuro ogni
tipo concreto del programma. In effetti, i tipi sicuri per la concorrenza sono l'eccezione piuttosto che la
regola, per cui si dovrebbe accedere a una variabile in modo concorrente solo se la documentazione del
suo tipo dice che è sicuro. Si evita l'accesso concorrente alla maggior parte delle variabili confinandole in
una singola goroutine o mantenendo un'invariante di livello superiore di mutua esclusione. Spiegheremo
questi termini in questo capitolo.
Al contrario, le funzioni esportate a livello di pacchetto devono essere generalmente sicure da
concurrency. Poiché le variabili a livello di pacchetto non possono essere confinate in una singola
goroutine, le funzioni che le modificano devono rispettare la mutua esclusione.
Ci sono molti motivi per cui una funzione potrebbe non funzionare quando viene chiamata in modo
concorrente, tra cui dead lock, livelock e resource starvation. Non abbiamo spazio per discuterli tutti,
quindi ci concentreremo su quello più importante, la condizione di gara.
Una condizione di gara è una situazione in cui il programma non fornisce il risultato corretto per alcune
interleavings delle operazioni di più goroutines. Le race condition sono perniciose perché possono
rimanere latenti in un programma e comparire di rado, magari solo sotto carico pesante o quando si
utilizzano determinati compilatori, piattaforme o architetture. Ciò le rende difficili da riprodurre e
diagnosticare.
È tradizione spiegare la gravità delle condizioni di gara attraverso la metafora della perdita finanziaria,
quindi prenderemo in considerazione un semplice programma di conto corrente.
// Il pacchetto banca implementa una banca con un solo conto. pacchetto
banca

var balance int

func Deposit(amount int) { balance = balance + amount } func


Balance() int { return balance }

(Avremmo potuto scrivere il corpo della funzione Deposito come saldo += importo, che è equivalente, ma
la forma più lunga semplificherà la spiegazione).
Per un programma così banale, possiamo vedere a colpo d'occhio che qualsiasi sequenza di chiamate a
Deposito e Saldo darà la risposta giusta, cioè i l Saldo riporterà la somma di tutti gli importi
precedentemente depositati. Tuttavia, se chiamiamo queste funzioni non in sequenza, ma in modo
concomitante, la risposta giusta non è più garantita. Consideriamo le due goroutine seguenti, che
rappresentano due transazioni su un conto bancario comune:
// Alice:
go func() {
banca.Deposito(200) // A1
fmt.Println("=", bank.Balance()) // A2
}()

www.it-ebooks.info
SEZIONE 9.1. CONDIZIONI DI GARA 259

// Bob:
go bank.Deposit(100) // B

Alice deposita 200 dollari, poi controlla il suo saldo, mentre Bob deposita 100 dollari. Poiché i passaggi
A1 e A2 avvengono in concomitanza con B, non possiamo prevedere l'ordine in cui avverranno.
Intuitivamente, potrebbe sembrare che ci siano solo tre ordini possibili, che chiameremo "prima Alice",
"prima Bob" e "Alice/Bob/Alice". La tabella seguente mostra il valore della variabile balance dopo ogni
passo. Le stringhe virgolettate rappresentano le schede di bilancio stampate.
Prima Alice Prima Bob Alice/Bob/Alice
0 0 0
A1 200 B 100 A1 200
A2 "= 200" A1 300 B 300
B 300 A2 "= 300" A2 "= 300"

In tutti i casi il saldo finale è di 300 dollari. L'unica variazione consiste nel fatto che la distinta di Alice
includa o meno la transazione di Bob, ma i clienti sono soddisfatti in ogni caso.
Ma questa intuizione è sbagliata. Esiste un quarto risultato possibile, in cui il deposito di Bob avviene nel
mezzo del deposito di Alice, dopo che il saldo è stato letto (saldo + importo) ma prima che sia stato
aggiornato (saldo = ...), causando la scomparsa della transazione di Bob. Questo perché l'operazione di
deposito A1 di Alice è in realtà una sequenza di due operazioni, una lettura e una scrittura; chiamiamole
A1r e A1w. Ecco l'interleaving problematico:
Gara di dati
0
A1r 0 ... = saldo + importo
B 100
A1w 200 equilibrio = ...
A2 " = 200"

Dopo A1r, l'espressione balance + amount viene valutata a 200, quindi questo è il valore scritto durante
A1w, nonostante il deposito intercorso. Il saldo finale è di soli 200 dollari. La banca è più ricca di 100
dollari a spese di Bob.
Questo programma contiene un particolare tipo di condizione di gara, chiamata gara di dati. Una gara
di dati si verifica quando due goroutine accedono contemporaneamente alla stessa variabile e almeno
uno degli accessi è una scrittura.
Le cose si complicano ulteriormente se la corsa ai dati coinvolge una variabile di tipo più grande di una
singola parola macchina, come un'interfaccia, una stringa o una slice. Questo codice aggiorna x in modo
simultaneo su due slice di lunghezza diversa:
var x []int
go func() { x = make([]int, 10) }()
go func() { x = make([]int, 1000000) }()
x[999999] = 1 // NOTA: comportamento non definito; possibile corruzione della memoria!

Il valore di x nell'istruzione finale non è definito; potrebbe essere nil, o una slice di lunghezza 10, o una
slice di lunghezza 1.000.000. Ma ricordiamo che una slice è composta da tre parti: il puntatore, la
lunghezza e la capacità. Se il puntatore proviene dalla prima chiamata da effettuare e la lunghezza
proviene da

www.it-ebooks.info
260 CAPITOLO 9. CONCORRENZA CON VARIABILI CONDIVISE

la seconda, x sarebbe una chimera, una fetta la cui lunghezza nominale è 1.000.000 ma il cui array
sottostante ha solo 10 elementi. In questa eventualità, la memorizzazione nell'elemento 999.999
distruggerebbe una posizione di memoria arbitraria e lontana, con conseguenze impossibili da prevedere
e difficili da debuggare e localizzare. Questo campo minato semantico si chiama comportamento
indefinito ed è ben noto ai programmatori di C; fortunatamente in Go non è così problematico come in
C.
Anche l'idea che un programma concorrente sia un intreccio di più programmi sequenziali è una falsa
intuizione. Come vedremo nella Sezione 9.4, i data race possono avere esiti ancora più strani. Molti
programmatori, anche molto intelligenti, di tanto in tanto offrono giustificazioni per i data race noti nei
loro programmi: "il costo della mutua esclusione è troppo alto", "questa logica è solo per il logging", "non
mi importa se lascio cadere alcuni messaggi" e così via. L'assenza di problemi su un determinato
compilatore e su una determinata piattaforma può dare una falsa sicurezza. Una buona regola è che non
esiste una corsa ai dati benigna. Quindi, come possiamo evitare i data race nei nostri programmi?
Ripetiamo la definizione, perché è molto importante: una corsa ai dati si verifica ogni volta che due
goroutine accedono contemporaneamente alla stessa variabile e almeno uno degli accessi è una scrittura.
Da questa definizione si evince che esistono tre modi per evitare una corsa ai dati.
Il primo modo è quello di non scrivere la variabile. Si consideri la mappa sottostante, che viene popolata
pigramente quando ogni chiave viene richiesta per la prima volta. Se Icon viene richiamato in modo
sequenziale, il programma funziona bene, ma se Icon viene richiamato in modo concorrente, si verifica
un data race nell'accesso alla mappa.
var icons = make(map[string]image.Image) func
loadIcon(name string) image.Image
// NOTA: non è sicuro per la concurrency!
func Icon(name string) image.Image { icon, ok
:= icons[name]
if !ok {
icona = loadIcon(nome) icone[nome] =
icona
}
icona di ritorno
}

Se invece inizializziamo la mappa con tutte le voci necessarie prima di creare altre goroutine e non la
modifichiamo mai più, allora un numero qualsiasi di goroutine può chiamare Icon in modo sicuro e
simultaneo, poiché ognuna legge solo la mappa.
var icons = map[string]image.Image{ "picche.png":
loadIcon("picche.png"),
"cuori.png": loadIcon("hearts.png"),
"diamonds.png": loadIcon("diamonds.png"),
"clubs.png": loadIcon("clubs.png"),
}

// A prova di concorrenza.
func Icon(name string) image.Image { return icons[name] }

www.it-ebooks.info
SEZIONE 9.1. CONDIZIONI DI GARA 261

Nell'esempio precedente, la variabile icons viene assegnata durante l'inizializzazione del pacchetto,
che avviene prima dell'avvio della funzione principale del programma. Una volta inizializzata, icons
non viene mai modificata. Le strutture di dati che non vengono mai modificate o che sono
immutabili sono intrinsecamente concur- rency-safe e non necessitano di sincronizzazione. Ma
ovviamente non possiamo usare questo approccio se gli aggiornamenti sono essenziali, come nel caso
di un conto bancario.
Il secondo modo per evitare una corsa ai dati è evitare di accedere alla variabile da più
goroutine. Questo è l'approccio adottato da molti dei programmi del capitolo precedente. Ad esempio, la
goroutine principale nel web crawler concorrente (§8.6) è l'unica goroutine che accede alla mappa seen, e
la goroutine broadcaster nel server di chat (§8.10) è l'unica goroutine che accede alla mappa clients.
Queste variabili sono confinate in un'unica goroutine.
Poiché le altre goroutine non possono accedere direttamente alla variabile, devono utilizzare un canale
per inviare alla goroutine confinante una richiesta di interrogazione o aggiornamento della variabile. È
questo il significato del mantra di Go: "Non comunicare condividendo la memoria; condividi la
memoria comunicando". Una goroutine che intermedia l'accesso a una variabile confinata utilizzando
richieste di canale è chiamata goroutine monitor per quella variabile. Ad esempio, la goroutine
broadcaster monitora l'accesso alla mappa dei client.
Ecco l'esempio della banca riscritto con la variabile balance confinata in una goroutine del monitor
chiamata teller:
gopl.io/ch9/bank1
// Il pacchetto bank fornisce una banca concurrency-safe con un conto. package bank
var deposits = make(chan int) // invia l'importo al deposito var
balances = make(chan int) // riceve il saldo
func Deposito(importo int) { depositi <- importo } func
Saldo() int { return <-saldo }
func teller() {
var balance int // il saldo è limitato alla goroutine degli sportelli
per {
selezionare {
caso importo := <depositi: saldo +=
importo
caso saldi <- saldo:
}
}
}
func init() {
go teller() // avvia la goroutine del monitor
}

Anche quando una variabile non può essere confinata a una singola goroutine per tutta la sua durata, il
confinamento può comunque essere una soluzione al problema dell'accesso concorrente. Ad esempio, è
comune condividere una variabile tra le goroutine di una pipeline, passando il suo indirizzo da uno
stadio al successivo attraverso un canale. Se ogni stadio della pipeline si astiene dall'accedere alla
variabile dopo che

www.it-ebooks.info
262 CAPITOLO 9. CONCORRENZA CON VARIABILI CONDIVISE

inviandola allo stadio successivo, tutti gli accessi alla variabile sono sequenziali. In effetti, la variabile è
confinata in uno stadio della pipeline, poi in quello successivo e così via. Questa disciplina viene talvolta
chiamata confinamento seriale.

Nell'esempio seguente, le torte sono confinate in serie, prima alla goroutine baker, poi alla goroutine
goroutine icer:

type Cake struct{ state string } func

baker(cooked chan<- *Cake) {


per {
torta := new(Torta)
torta.stato = "cotto"
cotto <- torta // il pasticciere non tocca più questa torta
}
}

func icer(iced chan<- *Cake, cooked <-chan *Cake) { for cake


:= range cooked {
cake.state = "glassato"
glassato <- torta // icer non tocca più questa torta
}
}

Il terzo modo per evitare una corsa ai dati consiste nel consentire a molte goroutine di accedere alla
variabile, ma solo a una alla volta. Questo approccio è noto come mutua esclusione ed è l'argomento
della prossima sezione.

Esercizio 9.1: Aggiungere una funzione Withdraw(amount int) bool al programma


gopl.io/ch9/bank1. Il risultato deve indicare se la transazione è riuscita o fallita a causa dell'insufficienza
di fondi. Il messaggio inviato alla goroutine monitor deve contenere sia l'importo da prelevare sia un
nuovo canale su cui la goroutine monitor possa inviare il risultato booleano a Withdraw.

9.2. Mutua esclusione: sync.Mutex

Nella Sezione 8.6 abbiamo utilizzato un canale buffer come semaforo di conteggio per garantire che non
più di 20 goroutine effettuassero richieste HTTP simultanee. Con la stessa idea, possiamo usare un
canale di capacità 1 per garantire che al massimo una goroutine acceda a una variabile condivisa alla
volta. Un semaforo che conta solo fino a 1 è chiamato semaforo binario.

gopl.io/ch9/bank2
var (
sema = make(chan struct{}, 1) // un semaforo binario a guardia dell'equilibrio
balance int
)

www.it-ebooks.info
SEZIONE 9.2. MUTUA ESCLUSIONE: SYNC.MUTEX 263

func Deposito(importo int) {


sema <- struct{}{} // acquisire il token
balance = balance + amount
<-sema // token di rilascio
}
func Balance() int {
sema <- struct{}{} // acquisire il token b
:= saldo
<-sema // rilasciare il token
return b
}

Questo schema di mutua esclusione è così utile che è supportato direttamente dal tipo Mutex del
pacchetto sync. Il suo metodo Lock acquisisce il token (chiamato lock) e il suo metodo Unlock lo rilascia:
gopl.io/ch9/bank3
importare "sync"
var (
mu sync.Mutex // guardie balance
balance int
)
func Deposito(importo int) {
mu.Lock()
saldo = saldo + importo mu.Unlock()
}
func Balance() int {
mu.Lock()
b := balance
mu.Unlock()
return b
}

Ogni volta che una goroutine accede alle variabili del banco (qui solo il saldo), deve chiamare il metodo
Lock del mutex per acquisire un blocco esclusivo. Se un'altra goroutine ha acquisito il blocco, questa
operazione si blocca finché l'altra goroutine non chiama Unlock e il blocco diventa nuovamente
disponibile. Il mutex protegge le variabili condivise. Per convenzione, le variabili protette da un mutex
sono dichiarate subito dopo la dichiarazione del mutex stesso. Se ci si discosta da questa regola, è bene
documentarla.
La regione di codice tra Lock e Unlock in cui una goroutine è libera di leggere e modificare le variabili
condivise è chiamata sezione critica. La chiamata del detentore del blocco a Unlock avviene prima che
qualsiasi altra goroutine possa acquisire il blocco per sé. È essenziale che la goroutine rilasci il blocco una
volta terminato, in tutti i percorsi della funzione, compresi quelli di errore.
Il programma della banca di cui sopra esemplifica un modello di concorrenza comune. Un insieme di
funzioni esportate incapsula una o più variabili, in modo che l'unico modo per accedere alle variabili sia
tramite

www.it-ebooks.info
264 CAPITOLO 9. CONCORRENZA CON VARIABILI CONDIVISE

queste funzioni (o metodi, per le variabili di un oggetto). Ogni funzione acquisisce un blocco mutex
all'inizio e lo rilascia alla fine, assicurando così che le variabili condivise non vengano accedute
simultaneamente. Questa disposizione di funzioni, lock mutex e variabili è chiamata monitor. (Questo
vecchio uso della parola "monitor" ha ispirato il termine "monitor goroutine". Entrambi gli usi
condividono il significato di intermediario che garantisce l'accesso sequenziale alle variabili).
Poiché le sezioni critiche delle funzioni Deposito e Saldo sono così brevi - una sola riga, senza
ramificazioni - la chiamata a Sblocca alla fine è semplice. Nelle sezioni critiche più complesse,
soprattutto quelle in cui gli errori devono essere gestiti con un ritorno anticipato, può essere difficile
capire che le chiamate a Lock e Unlock sono strettamente accoppiate su tutti i percorsi. L'istruzione
defer di Go viene in soccorso: rinviando una chiamata a Unlock, la sezione critica si estende
implicitamente fino alla fine della funzione corrente, liberandoci dal dover ricordare di inserire le
chiamate a Unlock in uno o più punti lontani dalla chiamata a Lock.
func Balance() int {
mu.Lock()
deferisci
mu.Unlock() return
balance
}

Nell'esempio precedente, lo sblocco viene eseguito dopo che l'istruzione return ha letto il valore di balance,
quindi la funzione Balance è concurrency-safe. I n o l t r e , la variabile locale b non è più necessaria.
Inoltre, uno sblocco differito viene eseguito anche se la sezione critica va in panico, il che può essere
importante nei programmi che fanno uso di recover (§5.10). Un rinvio è marginalmente più costoso
di una chiamata esplicita a Unlock, ma non abbastanza da giustificare un codice meno chiaro. Come
sempre nei programmi concorrenti, è bene privilegiare la chiarezza e resistere all'ottimizzazione
prematura. Ove possibile, utilizzare il defer e lasciare che le sezioni critiche si estendano fino alla
fine di una funzione.
Si consideri la funzione Prelievo qui sotto. In caso di successo, riduce il saldo dell'importo specificato e
restituisce true. Ma se il conto non dispone di fondi sufficienti per la transazione, Withdraw ripristina il
saldo e restituisce false.
// NOTA: non atomico!
func Withdraw(amount int) bool {
Deposito(-amount)
se Saldo() < 0 {
Deposito(importo)
return false // fondi insufficienti
}
restituire vero
}

Questa funzione alla fine fornisce il risultato corretto, ma ha un brutto effetto collaterale. Quando si
tenta un prelievo eccessivo, il saldo scende transitoriamente sotto lo zero. Questo può far sì che un
prelievo corrente per una somma modesta venga rifiutato in modo spurio. Così, se Bob cerca di
comprare un'auto sportiva, Alice non può pagare il caffè del mattino. Il problema è che il prelievo non è
atomico: consiste in una sequenza di tre operazioni distinte, ciascuna delle quali acquisisce e poi rilascia

www.it-ebooks.info
SEZIONE 9.2. MUTUA ESCLUSIONE: SYNC.MUTEX 265

il blocco del mutex, ma nulla blocca l'intera sequenza.

Idealmente, Withdraw dovrebbe acquisire il blocco mutex una sola volta per l'intera operazione.
Tuttavia, questo tentativo non funziona:

// NOTA: non è corretto!


func Withdraw(amount int) bool { mu.Lock()
deferisce
mu.Unlock()
Deposito(-importo) if
Balance() < 0 {
Deposito (importo)
return false // fondi insufficienti
}
restituire vero
}

Deposit tenta di acquisire il blocco del mutex una seconda volta chiamando mu.Lock(), ma poiché i
blocchi dei mutex non sono rientranti - non è possibile bloccare un mutex già bloccato - questo
porta a un deadlock in cui nulla può procedere e Withdraw si blocca per sempre.

C'è una buona ragione per cui i mutex di Go non sono rientranti. Lo scopo di un mutex è quello di
garantire che alcuni invarianti delle variabili condivise siano mantenuti in punti critici durante
l'esecuzione del programma. Uno degli invarianti è "nessuna goroutine sta accedendo alle variabili
condivise", ma ci possono essere altri invarianti specifici per le strutture di dati che il mutex protegge.
Quando una goroutine acquisisce un blocco mutex, può assumere che gli invarianti siano validi. Mentre
mantiene il blocco, può aggiornare le variabili condivise in modo che gli invarianti siano
temporaneamente violati. Tuttavia, quando rilascia il blocco, deve garantire che l'ordine sia stato
ripristinato e che gli invarianti siano n u o v a m e n t e validi. Sebbene un mutex rientrante garantisca
che nessun'altra goroutine acceda alle variabili condivise, non può proteggere gli invarianti aggiuntivi di
tali variabili.

Una soluzione comune è quella di dividere una funzione come Deposito in due: una funzione non
esportata, Deposito, che assume che il blocco sia già in possesso e svolge il lavoro vero e proprio, e
una funzione esportata Deposito che acquisisce il blocco prima di chiamare Deposito. Possiamo
quindi esprimere Withdraw in termini di deposit come segue:

func Withdraw(amount int) bool { mu.Lock()


deferisce
mu.Unlock()
deposit(-amount) if
balance < 0 {
deposito(importo)
return false // fondi insufficienti
}
restituire vero
}

www.it-ebooks.info
266 CAPITOLO 9. CONCORRENZA CON VARIABILI CONDIVISE

func Deposito(importo int) {


mu.Lock()
rinviare mu.Unlock()
deposit(amount)
}
func Balance() int {
mu.Lock()
deferisci
mu.Unlock() return
balance
}
// Questa funzione richiede che il blocco sia mantenuto.
func deposit(amount int) { balance += amount }

Naturalmente, la funzione di deposito mostrata qui è così banale che una funzione di prelievo
realistica non si preoccuperebbe di chiamarla, ma comunque illustra il principio.
L'incapsulamento (§6.6), riducendo le interazioni inattese in un programma, ci aiuta a mantenere gli
invarianti della struttura dei dati. Per lo stesso motivo, l'incapsulamento aiuta anche a mantenere gli
invarianti di concomitanza. Quando si usa un mutex, bisogna assicurarsi che sia esso che le variabili che
protegge non siano esportate, sia che si tratti di variabili a livello di pacchetto che di campi di una
struttura.

9.3. Mutex di lettura/scrittura: sync.RWMutex

In preda all'ansia dopo aver visto il suo deposito di 100 dollari sparire senza lasciare traccia, Bob scrive
un programma per controllare il suo saldo bancario centinaia di volte al secondo. Lo esegue a casa, al
lavoro e sul telefono. La banca si accorge che l'aumento del traffico ritarda i depositi e i prelievi, perché
tutte le richieste di saldo vengono eseguite in sequenza, mantenendo esclusivamente il blocco e
impedendo t e m p o r a n e a m e n t e l'esecuzione di altre goroutine.
Dato che la funzione Balance deve solo leggere lo stato della variabile, sarebbe sicuro che più chiamate
Balance vengano eseguite contemporaneamente, a patto che non siano in esecuzione chiamate di deposito
o prelievo. In questo scenario abbiamo bisogno di un tipo speciale di blocco che permetta alle operazioni
di sola lettura di procedere in parallelo tra loro, ma alle operazioni di scrittura di avere un accesso
completamente esclusivo. Questo blocco è chiamato blocco per lettori multipli e scrittori singoli e in Go è
fornito da sync.RWMutex:
var mu sync.RWMutex var
balance int
func Balance() int { mu.RLock() // il
blocco dei lettori viene
rinviato mu.RUnlock()
bilancio di ritorno
}

La funzione Saldo chiama ora i metodi RLock e RUnlock per acquisire e rilasciare un blocco lettore o
condiviso. La funzione Deposito, che rimane invariata, chiama i metodi mu.Lock e mu.Unlock per
acquisire e rilasciare un blocco di scrittura o esclusivo.

www.it-ebooks.info
SEZIONE 9.4. SINCRONIZZAZIONE DELLA 267
MEMORIA

Dopo questa modifica, la maggior parte delle richieste di saldo di Bob vengono eseguite in parallelo
e si concludono più rapidamente. Il blocco è disponibile per una parte maggiore del tempo e le
richieste di deposito possono procedere in modo tempestivo.
RLock può essere usato solo se non ci sono scritture su variabili condivise nella sezione critica. In
generale, non si deve presumere che le funzioni o i metodi di sola lettura non aggiornino anche alcune
variabili. Per esempio, un metodo che sembra un semplice acces- sore potrebbe anche incrementare un
contatore di utilizzo interno o aggiornare una cache in modo che le chiamate ripetute siano più veloci.
In caso di dubbio, utilizzare un blocco esclusivo.
È vantaggioso usare un RWMutex solo quando la maggior parte delle goroutine che acquisiscono il blocco
sono lettori e il blocco è in contesa, cioè le goroutine devono aspettare abitualmente per acquisirlo. Un
RWMutex richiede una contabilità interna più complessa, che lo rende più lento di un normale mutex per
i lock senza contesa.

9.4. Memoria Sincronizzazione

Ci si può chiedere perché il metodo Balance necessiti di mutua esclusione, sia essa basata su canali o
mutex. Dopo tutto, a differenza di Deposito, consiste in una sola operazione, quindi non c'è il pericolo
che un'altra goroutine esegua ''nel mezzo'' di essa. Ci sono due ragioni per cui abbiamo bisogno di un
mutex. Il primo è che è altrettanto importante che Balance non venga eseguito nel mezzo di un'altra
operazione come Withdraw. La seconda ragione (più sottile) è che la sincronizzazione non riguarda solo
l'ordine di esecuzione di più goroutine; la sincronizzazione riguarda anche la memoria.
In un computer moderno possono esserci decine di processori, ciascuno con la propria cache locale della
memoria principale. Per efficienza, le scritture in memoria vengono bufferizzate all'interno di ciascun
processore e riversate nella memoria principale solo quando necessario. Le scritture possono anche
essere trasferite alla memoria principale in un ordine diverso da quello in cui sono state scritte dalla
goroutine di scrittura. Le primitive di sincronizzazione, come le comunicazioni di canale e le operazioni
mutex, fanno sì che il processore esegua il flush out e il commit di tutte le scritture accumulate, in modo
da garantire che gli effetti dell'esecuzione della goroutine fino a quel momento siano visibili alle
goroutine in esecuzione su altri processori.
Considerate i possibili risultati del seguente frammento di codice:
var x, y int go
func() {
x = 1 // A1
fmt.Print("y:", y, " ") // A2
}()
go func() {
y = 1 // B1
fmt.Print("x:", x, " ") // B2
}()

Poiché queste due goroutine sono concorrenti e accedono a variabili condivise senza mutua esclusione, si
verifica una corsa ai dati, per cui non ci si deve sorprendere che il programma non sia

www.it-ebooks.info
268 CAPITOLO 9. CONCORRENZA CON VARIABILI CONDIVISE

deterministico. Potremmo aspettarci che stampi uno qualsiasi di questi quattro risultati, che
corrispondono a interlacciamenti intuitivi delle istruzioni etichettate del programma:
y:0 x:1
x:0 y:1
x:1 y:1
y:1 x:1

La quarta riga potrebbe essere spiegata dalla sequenza A1,B1,A2,B2 o da B1,A1,A2,B2, ad esempio. Tuttavia,
questi due risultati potrebbero sorprendere:
x:0 y:0
y:0 x:0

ma a seconda del compilatore, della CPU e di molti altri fattori, possono anche verificarsi. Quale
potrebbe essere il possibile intreccio delle quattro istruzioni per spiegarle?

All'interno di una singola goroutine, gli effetti di ogni istruzione sono garantiti nell'ordine di esecuzione;
le goroutine sono sequenzialmente coerenti. Tuttavia, in assenza di una sincronizzazione esplicita tramite
un canale o un mutex, non è garantito che gli eventi siano visti nello stesso ordine da tutte le goroutine.
Sebbene la goroutine A debba osservare l'effetto della scrittura x = 1 prima di leggere il valore di y, non
osserva necessariamente la scrittura su y effettuata dalla goroutine B, quindi A potrebbe stampare un
valore di y non aggiornato.

Si è tentati di comprendere la concomitanza come se corrispondesse a un'intersezione degli enunciati di


ciascuna goroutine, ma come mostra l'esempio precedente, questo non è il modo in cui funziona un
moderno compilatore o una CPU. Poiché l'assegnazione e la stampa si riferiscono a variabili diverse, un
compilatore può concludere che l'ordine delle due istruzioni non può influire sul risultato e le scambia.
Se le due goroutine vengono eseguite su CPU diverse, ciascuna con la propria cache, le scritture di una
goroutine non sono visibili alla stampa dell'altra goroutine finché le cache non vengono sincronizzate
con la memoria principale.

Tutti questi problemi di concorrenza possono essere evitati con l'uso coerente di schemi semplici e
consolidati. Se possibile, limitate le variabili a una singola goroutine; per tutte le altre variabili, usate la
mutua esclusione.

9.5. Inizializzazione pigra: sync.Once

È buona norma rimandare una fase di inizializzazione costosa fino al momento in cui è necessaria.
Inizializzare una variabile in anticipo aumenta la latenza di avvio di un programma ed è inutile se
l'esecuzione non raggiunge sempre la parte del programma che utilizza quella variabile. Torniamo alla
variabile icone che abbiamo visto all'inizio del capitolo:
var icons map[string]image.Image

Questa versione di Icon utilizza l'inizializzazione pigra:

www.it-ebooks.info
SEZIONE 9.5. INIZIALIZZAZIONE PIGRA: SYNC.ONCE 269

func loadIcons() {
icone = map[string]image.Image{ "picche.png":
loadIcon("picche.png"), "cuori.png":
loadIcon("hearts.png"), "diamonds.png":
loadIcon("diamonds.png"), "clubs.png":
loadIcon("clubs.png"),
}
}

// NOTA: non è sicuro per la concurrency!


func Icon(name string) image.Image { if
icons == nil {
loadIcons() // inizializzazione una tantum
}
return icone[nome]
}

Per una variabile a cui accede una sola goroutine, possiamo usare lo schema precedente, ma questo
metodo non è sicuro se Icon viene chiamata in modo concorrente. Come la funzione Deposito originale
della banca, Icon è composta da più passaggi: verifica se icone è nullo, quindi carica le icone, quindi
aggiorna icone a un valore non nullo. L'intuizione potrebbe suggerire che il peggior risultato possibile
della condizione di gara di cui sopra è che la funzione loadIcons venga chiamata più volte.
Mentre la prima goroutine è impegnata a caricare le icone, un'altra goroutine che entra in Icon
troverebbe la variabile ancora uguale a nil e chiamerebbe anch'essa loadIcons.

Ma anche questa intuizione è sbagliata. (Speriamo che a questo punto stiate sviluppando una nuova
intuizione sulla concorrenza, ovvero che non ci si può fidare delle intuizioni sulla concorrenza).
Ricordiamo la discussione sulla memoria della Sezione 9.4. In assenza di una sincronizzazione esplicita,
il compilatore e la CPU sono liberi di riordinare gli accessi alla memoria in qualsiasi modo, purché il
comportamento di ogni goroutine sia sequenzialmente coerente. Un possibile riordino delle istruzioni di
loadIcons è mostrato di seguito. Il programma memorizza la mappa vuota nella variabile icons prima di
popolarla:

func loadIcons() {
icone = make(map[string]image.Image) icone["picche.png"] =
loadIcon("picche.png") icone["cuori.png"] =
loadIcon("cuori.png") icone["diamanti.png"] =
loadIcon("diamanti.png") icone["club.png"] =
loadIcon("club.png")
}

Di conseguenza, una goroutine che trova icone non nulle non può assumere che l'inizializzazione della
variabile sia completa.

Il modo più semplice e corretto per garantire che tutte le goroutine osservino gli effetti di loadIcons è quello di
sincronizzarle utilizzando un mutex:

var mu sync.Mutex // guardie icone var icons


map[string]image.Image

www.it-ebooks.info
270 CAPITOLO 9. CONCORRENZA CON VARIABILI CONDIVISE

// A prova di concorrenza.
func Icon(name string) image.Image { mu.Lock()
deferisce
mu.Unlock() if icons
== nil {
loadIcons()
}
return icone[nome]
}

Tuttavia, il costo di imporre l'accesso reciprocamente esclusivo alle icone è che due goroutine non
possono accedere alla variabile in modo simultaneo, anche quando la variabile è stata inizializzata in
modo sicuro e non sarà mai più modificata. Questo suggerisce un blocco a lettura multipla:
var mu sync.RWMutex // icone delle guardie
var icons map[string]image.Image

// A prova di concorrenza.
func Icon(name string) image.Image { mu.RLock()
if icone != nil {
icona := icone[nome]
mu.RUnlock()
icona di ritorno
}
mu.RUnlock()

// acquisire un blocco esclusivo


mu.Lock()
if icons == nil { // NOTA: deve essere ricontrollato se
loadIcons() è nullo.
}
icona := icone[nome]
mu.Sblocca()
icona di ritorno
}

Ora ci sono due sezioni critiche. La goroutine acquisisce prima un blocco del lettore, consulta la mappa e
rilascia il blocco. Se è stata trovata una voce (caso comune), questa viene restituita. Se non è stata trovata
nessuna voce, la goroutine acquisisce un blocco di scrittura. Non c'è modo di passare da un blocco
condiviso a uno esclusivo senza prima rilasciare il blocco condiviso, quindi dobbiamo ricontrollare la
variabile icone nel caso in cui un'altra goroutine l'abbia già inizializzata nel frattempo.

Lo schema precedente offre una maggiore concurrency, ma è complesso e quindi soggetto a errori.
Fortunatamente, il pacchetto sync fornisce una soluzione specializzata al problema dell'inizializzazione
una tantum: sync.Once. Concettualmente, una Once consiste in un mutex e in una variabile booleana
che registra se l'inizializzazione ha avuto luogo; il mutex protegge sia la booleana che le strutture
dati del client. L'unico metodo, Do, accetta come argomento la funzione di inizializzazione.
Utilizziamo Once per semplificare la funzione Icon:

www.it-ebooks.info
SEZIONE 9.6. IL RILEVATORE DI GARA 271

var loadIconsOnce sync.Once


var icons map[string]image.Image

// A prova di concorrenza.
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons) return
icons[name]
}

Ogni chiamata a Do(loadIcons) blocca il mutex e controlla la variabile booleana. Alla prima chiamata,
in cui la variabile è falsa, Do chiama loadIcons e imposta la variabile su true. Le chiamate successive
non fanno nulla, ma la sincronizzazione del mutex assicura che gli effetti di loadIcons sulla memoria
(in particolare sulle icone) siano visibili a tutte le goroutine. Utilizzando sync.Once in questo modo,
possiamo evitare di condividere le variabili con altre goroutine fino a quando non sono state
strutturate correttamente.
Esercizio 9.2: Riscrivere l'esempio PopCount della Sezione 2.6.2 in modo che inizializzi la tabella di
ricerca utilizzando sync.Once la prima volta che è necessario. (Realisticamente, il costo della
sincronizzazione sarebbe proibitivo per una funzione piccola e altamente ottimizzata come PopCount).

9.6. Il rivelatore di gara

Anche con la massima attenzione, è fin troppo facile commettere errori di concorrenza. Fortunatamente,
il runtime e la toolchain di Go sono dotati di uno strumento di analisi dinamica sofisticato e facile da
usare, il race detector.
È sufficiente aggiungere il flag -race al comando go build, go run o go test. Questo fa sì che il
compilatore costruisca una versione modificata dell'applicazione o del test con una strumentazione
aggiuntiva che registra effettivamente tutti gli accessi a variabili condivise avvenuti durante l'esecuzione,
insieme all'identità della goroutine che ha letto o scritto la variabile. Inoltre, il programma modificato
registra tutti gli eventi di sincronizzazione, come le istruzioni go, le operazioni di canale e le chiamate a
(*sync.Mutex).Lock, (*sync.WaitGroup).Wait e così via. (L'insieme completo degli eventi di
sincronizzazione è specificato nel documento The Go Memory Model che accompagna le specifiche del
linguaggio).
Il race detector studia questo flusso di eventi, cercando i casi in cui una goroutine legge o scrive una
variabile condivisa che è stata scritta più di recente da un'altra goroutine senza un'operazione di
sincronizzazione intermedia. Ciò indica un accesso concorrente alla variabile condivisa e quindi una
corsa ai dati. Lo strumento stampa un rapporto che include l'identità della variabile e gli stack delle
chiamate di funzione attive nella goroutine di lettura e nella goroutine di scrittura. Questo è solitamente
sufficiente per individuare il problema. La sezione 9.7 contiene un esempio di race detector in azione.
Il rilevatore di gare riporta tutte le gare di dati che sono state effettivamente eseguite. Tuttavia, è in grado
di rilevare solo le condizioni di gara che si verificano durante l'esecuzione; non può dimostrare che non
si verificheranno mai. Per ottenere risultati ottimali, assicurarsi che i test esercitino i pacchetti
utilizzando la concorrenza.

www.it-ebooks.info
272 CAPITOLO 9. CONCORRENZA CON VARIABILI CONDIVISE

A causa della contabilità aggiuntiva, un programma costruito con il rilevamento delle gare ha bisogno di
più tempo e memoria per essere eseguito, ma l'overhead è tollerabile anche per molti lavori di
produzione. Per le condizioni di gara che si verificano raramente, lasciare che il rilevatore di gare faccia il
suo lavoro può far risparmiare ore o giorni di debug.

9.7. Esempio: Cache concomitante non bloccante

In questa sezione, costruiremo una cache concorrente non bloccante, un'astrazione che risolve un
problema che si presenta spesso nei programmi concorrenziali del mondo reale, ma che non è ben
affrontato dalle librerie esistenti. Si tratta del problema della memoizzazione di una funzione, cioè della
memorizzazione nella cache del risultato di una funzione in modo che debba essere calcolato una sola
volta. La nostra soluzione sarà sicura dal punto di vista della concorrenza ed eviterà la contesa associata ai
progetti basati su un singolo blocco per l'intera cache.

Utilizzeremo la funzione httpGetBody come esempio del tipo di funzione che potremmo voler
memorizzare. Questa funzione effettua una richiesta HTTP GET e legge il corpo della richiesta. Le
chiamate a questa funzione sono relativamente costose, quindi vorremmo evitare di ripeterle
inutilmente.
func httpGetBody(url string) (interface{}, error) { resp, err :=
http.Get(url)
if err != nil { return
nil, err
}
rinviare resp.Body.Close()
restituire ioutil.ReadAll(resp.Body)
}

L'ultima riga nasconde una piccola sottigliezza. ReadAll restituisce due risultati, un []byte e un
errore, ma poiché questi sono assegnabili ai tipi di risultato dichiarati di httpGetBody -
rispettivamente Interface{} ed Errore - possiamo restituire il risultato della chiamata senza ulteriori
indugi. Abbiamo scelto questo tipo di ritorno per httpGetBody in modo che sia conforme al tipo di
funzioni che la nostra cache è progettata per memorizzare.

Ecco la prima bozza della cache:

gopl.io/ch9/memo1
// Il pacchetto memo fornisce un metodo non sicuro per la concurrency
// memorizzazione di una funzione di tipo Func.
pacchetto memo

// Un Memo memorizza i risultati della chiamata di una


Func. type Memo struct {
f Funzione
cache map[string]result
}

// Func è il tipo di funzione da memorizzare. type Func


func(key string) (interface{}, error)

www.it-ebooks.info
SEZIONE 9.7. ESEMPIO: CACHE CONCORRENTE NON BLOCCANTE 273

tipo result struct {


value interface{}
err errore
}

func New(f Func) *Memo {


return &Memo{f: f, cache: make(map[string]result)}
}

// NOTA: non è sicuro per la concurrency!


func (memo *Memo) Get(key string) (interface{}, error) { res, ok :=
memo.cache[key]
if !ok {
res.value, res.err = memo.f(key)
memo.cache[key] = res
}
restituire res.value, res.err
}

Un'istanza di Memo contiene la funzione f da memorizzare, di tipo Func, e la cache, che è una mappatura
da stringhe a risultati. Ogni risultato è semplicemente la coppia di risultati restituiti da una chiamata a
f: un valore e un errore. Nel corso del progetto mostreremo diverse varianti di Memo, ma tutte
condivideranno questi aspetti di base.

Di seguito è riportato un esempio di utilizzo di Memo. Per ogni elemento di un flusso di URL in entrata,
chiamiamo Get, registrando la latenza della chiamata e la quantità di dati restituiti:

m := memo.New(httpGetBody)
for url := range incomingURLs() { start :=
time.Now()
value, err := m.Get(url) if
err := nil {
log.Print(err)
}
fmt.Printf("%s, %s, %d bytes\n",
url, time.Since(start), len(value.([]byte)))
}

Possiamo usare il pacchetto di test (argomento del Capitolo 11) per analizzare sistematicamente l'effetto
della memorizzazione. Dall'output del test qui sotto, vediamo che il flusso di URL contiene duplicati e
che, sebbene la prima chiamata a (*Memo).Get per ogni URL richieda centinaia di millisecondi, la
seconda richiesta restituisce la stessa quantità di dati in meno di un millisecondo.

$ go test -v gopl.io/ch9/memo1
=== ESEGUI Test
https://golang.org, 175.026418ms, 7537 byte https://godoc.org,
172.686825ms, 6878 byte https://play.golang.org, 115.762377ms,
5767 byte http://gopl.io, 749.887242ms, 2856 byte

www.it-ebooks.info
274 CAPITOLO 9. CONCORRENZA CON VARIABILI CONDIVISE

https://golang.org, 721ns, 7537 byte


https://godoc.org, 152ns, 6878 byte
https://play.golang.org, 205ns, 5767 byte
http://gopl.io, 326ns, 2856 byte
--- PASS: Test (1.21s) PASS
ok gopl.io/ch9/memo1 1.257s

Questo test esegue tutte le chiamate a Get in modo sequenziale.

Poiché le richieste HTTP sono una grande opportunità per il parallelismo, cambiamo il test in modo che
faccia tutte le richieste in modo concorrente. Il test utilizza un sync.WaitGroup per attendere il
completamento dell'ultima richiesta prima di tornare.

m := memo.New(httpGetBody) var n
sync.WaitGroup
per url := range incomingURLs() { n.Add(1)
go func(url string) { start
:= time.Now()
value, err := m.Get(url) if
err := nil {
log.Print(err)
}
fmt.Printf("%s, %s, %d bytes\n",
url, time.Since(start), len(value.([]byte))) n.Done()
}(url)
}
n.Wait()

Il test viene eseguito molto più velocemente, ma purtroppo è improbabile che funzioni sempre
correttamente. È possibile che si verifichino inaspettate mancanze della cache, o hit della cache che
restituiscono valori errati, o addirittura crash.

Peggio ancora, è probabile che per una parte del tempo funzioni correttamente, per cui potremmo anche
non accorgerci del problema. Ma se lo si esegue con il flag -race, il rilevatore di corse (§9.6)
spesso stampa un rapporto come questo:

$ go test -run=TestConcurrent -race -v gopl.io/ch9/memo1


=== ESEGUI TestConcorrente
...
AVVISO: CORSA AI DATI
Scrittura da parte della goroutine 36:
runtime.mapassign1()
~/go/src/runtime/hashmap.go:411 +0x0
gopl.io/ch9/memo1.(*Memo).Get()
~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205
...

www.it-ebooks.info
SEZIONE 9.7. ESEMPIO: CACHE CONCORRENTE NON BLOCCANTE 275

Scrittura precedente di goroutine 35:


runtime.mapassign1()
~/go/src/runtime/hashmap.go:411 +0x0
gopl.io/ch9/memo1.(*Memo).Get()
~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205
...
Trovata 1 razza(e) di dati
FALLIMENTO gopl.io/ch9/memo1 2.393s

Il riferimento a memo.go:32 ci dice che due goroutine hanno aggiornato la mappa della cache senza alcun
intervento di sincronizzazione. Get non è concurrency-safe: ha un data race.
28 func (memo *Memo) Get(stringa di chiave) (interface{}, error) {
29 res, ok := memo.cache[key]
30 if !ok {
31 res.value, res.err = memo.f(key)
32 memo.cache[key] = res
33 }
34 restituire res.value, res.err
35 }

Il modo più semplice per rendere la cache concurrency-safe è utilizzare la sincronizzazione basata sul
monitor. Basta aggiungere un mutex alla Memo, acquisire il blocco del mutex all'inizio di Get e rilasciarlo
prima del ritorno di Get, in modo che le due operazioni sulla cache avvengano all'interno della sezione
critica:

gopl.io/ch9/memo2
tipo Memo struct { f
Func
mu sync.Mutex // guardie cache
cache map[string]result
}

// Ottenere è sicuro dal punto di vista della concorrenza.


func (memo *Memo) Get(key string) (value interface{}, err error) { memo.mu.Lock()
res, ok := memo.cache[key] if
!ok {
res.value, res.err = memo.f(key)
memo.cache[key] = res
}
memo.mu.Unlock()
restituire res.value, res.err
}

Ora il rilevatore di corse è silenzioso, anche quando i test vengono eseguiti in contemporanea.
Sfortunatamente, questa modifica a Memo inverte i guadagni di prestazioni ottenuti in precedenza.
Mantenendo il blocco per la durata di ogni chiamata a f, Get serializza tutte le operazioni di I/O che
intendevamo parallelizzare. Abbiamo bisogno di u n a cache non bloccante, che non serializzi le
chiamate alla funzione che memorizza.

www.it-ebooks.info
276 CAPITOLO 9. CONCORRENZA CON VARIABILI CONDIVISE

Nella prossima implementazione di Get, riportata di seguito, la goroutine chiamante acquisisce il blocco
due volte: una prima volta per la ricerca e una seconda volta per l'aggiornamento se la ricerca non ha
dato alcun risultato. Nel frattempo, le altre goroutine sono libere di utilizzare la cache.
gopl.io/ch9/memo3
func (memo *Memo) Get(key string) (value interface{}, err error) { memo.mu.Lock()
res, ok := memo.cache[key] memo.mu.Unlock()
if !ok {
res.value, res.err = memo.f(key)

// Tra le due sezioni critiche, diverse goroutine


// potrebbe essere una corsa per calcolare f(chiave) e aggiornare la mappa.
memo.mu.Lock()
memo.cache[key] = res
memo.mu.Unlock()
}
restituire res.value, res.err
}

Le prestazioni migliorano ancora, ma ora notiamo che alcuni URL vengono recuperati due volte. Questo
accade quando due o più goroutine chiamano Get per lo stesso URL più o meno nello stesso momento.
Entrambe consultano la cache, non trovano alcun valore e quindi chiamano la funzione lenta f. Poi
entrambe aggiornano la mappa con il risultato ottenuto. Uno dei risultati viene sovrascritto dall'altro.

L'ideale sarebbe evitare questo lavoro ridondante. Questa caratteristica viene talvolta chiamata
soppressione dei duplicati. Nella versione di Memo che segue, ogni elemento della mappa è un
puntatore a una struttura di ingresso. Ogni voce contiene il risultato memorizzato di una chiamata
alla funzione f, come prima, ma contiene anche un canale chiamato ready. Subito dopo che il
risultato della voce è stato impostato, questo canale viene chiuso, per trasmettere (§8.9) a tutte le
altre goroutine che ora è sicuro leggere il risultato dalla voce.
gopl.io/ch9/memo4
tipo entry struct {
res risultato
ready chan struct{} // chiuso quando la res è pronta
}

func New(f Func) *Memo {


return &Memo{f: f, cache: make(map[string]*entry)}
}

tipo Memo struct { f


Func
mu sync.Mutex // guardie cache
cache map[string]*entry
}

www.it-ebooks.info
SEZIONE 9.7. ESEMPIO: CACHE CONCORRENTE NON BLOCCANTE 277

func (memo *Memo) Get(key string) (value interface{}, err error) { memo.mu.Lock()
e := memo.cache[key] if
e == nil {
// Questa è la prima richiesta per questa chiave.
// Questa goroutine diventa responsabile del calcolo di
// il valore e trasmettere la condizione di pronto. e =
&entry{ready: make(chan struct{})} memo.cache[key] = e
memo.mu.Unlock()

e.res.value, e.res.err = memo.f(key) close(e.ready) //

trasmettere la condizione di ready


} else {
// Questa è una richiesta ripetuta per questa chiave.
memo.mu.Unlock()

<-e.ready // aspetta la condizione di ready


}
restituire e.res.value, e.res.err
}

Una chiamata a Get ora comporta l'acquisizione del blocco mutex che protegge la mappa della cache, la
ricerca nella mappa di un puntatore a una voce esistente, l'allocazione e l'inserimento di una nuova
voce se non è stata trovata, quindi il rilascio del blocco. Se c'è una voce esistente, il suo valore non è
necessariamente ancora pronto - un'altra goroutine potrebbe ancora chiamare la funzione lenta f - e
quindi la goroutine chiamante deve attendere la condizione di ''pronto'' della voce prima di leggerne
il risultato. Ciò avviene leggendo un valore dal canale pronto, poiché questa operazione si blocca finché
il canale non viene chiuso.
Se non c'è una voce esistente, inserendo una nuova voce ''non pronta'' nella mappa, la goroutine
corrente diventa responsabile dell'invocazione della funzione lenta, dell'aggiornamento della voce e
della trasmissione della disponibilità della nuova voce a qualsiasi altra goroutine che potrebbe (a quel
punto) essere in attesa.
Si noti che le variabili e.res.value ed e.res.err nella voce sono condivise tra più goroutine. La
goroutine che crea la voce imposta i loro valori e le altre goroutine li leggono una volta trasmessa la
condizione ''ready''. Nonostante l'accesso da parte di più goroutine, non è necessario un blocco mutex.
La chiusura del canale ready avviene prima che qualsiasi altra goroutine riceva l'evento di broadcast,
quindi la scrittura delle variabili nella prima goroutine avviene prima che vengano lette dalle goroutine
successive. Non c'è gara di dati.
La nostra cache concorrente, con soppressione dei duplicati e non bloccante è completa.
L'implementazione di Memo qui sopra utilizza un mutex per proteggere una variabile mappa condivisa da
ogni goroutine che chiama Get. È interessante contrapporre questo progetto a uno alternativo in cui la
variabile map è confinata a una goroutine monitor a cui i chiamanti di Get devono inviare un messaggio.

www.it-ebooks.info
278 CAPITOLO 9. CONCORRENZA CON VARIABILI CONDIVISE

Le dichiarazioni di Func, result e entry rimangono come prima:


// Func è il tipo di funzione da memorizzare. type Func
func(key string) (interface{}, error)
// Un risultato è il risultato della chiamata di una
Func. type result struct {
valore interfaccia{}
err errore
}
tipo entry struct {
res risultato
ready chan struct{} // chiuso quando la res è pronta
}

Tuttavia, il tipo Memo ora consiste in un canale, requests, attraverso il quale il chiamante di Get comunica
con la goroutine Monitor. Il tipo di elemento del canale è una richiesta. Utilizzando questa struttura, il
chiamante di Get invia alla goroutine monitor sia la chiave, cioè l'argomento della funzione
memorizzata, sia un altro canale, response, attraverso il quale il risultato deve essere rinviato quando
diventa disponibile. Questo canale trasporta solo un singolo valore.
gopl.io/ch9/memo5
// Una richiesta è un messaggio che richiede l'applicazione della Func alla chiave. type
request struct {
chiave stringa
response chan<- result // il cliente vuole un singolo risultato
}
tipo Memo struct{ requests chan request }
// New restituisce una memorizzazione di f. I client devono successivamente chiamare
Close. func New(f Func) *Memo {
memo := &Memo{richieste: make(richiesta chan)} go
memo.server(f)
restituire il promemoria
}
func (memo *Memo) Get(key string) (interface{}, error) { response :=
make(chan result)
memo.requests <- request{key, response} res := <-
response
restituire res.value, res.err
}
func (memo *Memo) Close() { close(memo.requests) }

Il metodo Get, qui sopra, crea un canale di risposta, lo inserisce nella richiesta, lo invia alla goroutine
moni- tor e lo riceve immediatamente.
La variabile cache è confinata nella goroutine monitor (*Memo).server, mostrata di seguito. Il
monitor legge le richieste in un ciclo fino a quando il canale delle richieste viene chiuso dal metodo
Close. Per ogni richiesta, consulta la cache, creando e inserendo una nuova voce se non è stata
trovata.

www.it-ebooks.info
SEZIONE 9.7. ESEMPIO: CACHE CONCORRENTE NON BLOCCANTE 279

func (memo *Memo) server(f Func) { cache


:= make(map[string]*entry) for req :=
range memo.requests {
e := cache[req.key] if
e == nil {
// Questa è la prima richiesta per questa
chiave. e = &entry{ready: make(chan struct{})}
cache[req.key] = e
go e.call(f, req.key) // chiama f(key)
}
go e.deliver(req.response)
}
}

func (e *entry) call(f Func, key string) {


// Valutare la funzione. e.res.value,
e.res.err = f(key)
// Trasmette la condizione di pronto.
close(e.ready)
}

func (e *entry) deliver(response chan<- result) {


// Attendere la condizione di pronto.
<-e.ready
// Inviare il risultato al client. response
<- e.res
}

In modo simile alla versione basata su mutex, la prima richiesta di una determinata chiave diventa
responsabile della chiamata della funzione f su quella chiave, della memorizzazione del risultato
nella voce e del casting ampio della disponibilità della voce chiudendo il canale ready. Questo
viene fatto da (*entry).call.
Una richiesta successiva per la stessa chiave trova la voce esistente nella mappa, attende che il risultato
sia pronto e lo invia attraverso il canale di risposta alla goroutine del client che ha chiamato Get.
Questo viene fatto da (*entry).deliver. I metodi call e deliver devono essere chiamati nelle proprie
goroutine, per garantire che la goroutine monitor non smetta di elaborare le nuove richieste.
Questo esempio dimostra che è possibile costruire molte strutture concorrenti utilizzando uno dei due
approcci - variabili e blocchi condivisi o processi sequenziali comunicanti - senza eccessiva complessità.
Non è sempre ovvio quale approccio sia preferibile in una determinata situazione, ma vale la pena sapere
come si corrispondono. A volte il passaggio da un approccio all'altro può semplificare il codice.
Esercizio 9.3: Estendere il tipo Func e il metodo (*Memo).Get in modo che i chiamanti possano fornire un
canale opzionale done attraverso il quale annullare l'operazione (§8.9). I risultati di una chiamata Func
annullata non devono essere memorizzati nella cache.

www.it-ebooks.info
280 CAPITOLO 9. CONCORRENZA CON VARIABILI CONDIVISE

9.8. Goroutines e Threads

Nel capitolo precedente abbiamo detto che la differenza tra goroutine e thread del sistema operativo
(OS) poteva essere ignorata fino a un secondo momento. Sebbene le differenze siano essenzialmente
quantitative, una differenza quantitativa abbastanza grande diventa qualitativa, e così è per le goroutine e
i thread. È giunto il momento di distinguerli.

9.8.1. Pile coltivabili

Ogni thread del sistema operativo ha un blocco di memoria di dimensioni fisse (spesso fino a 2 MB) per
il suo stack, l'area di lavoro in cui salva le variabili locali delle chiamate di funzione in corso o
temporaneamente sospese mentre viene chiamata un'altra funzione. Questo stack di dimensioni fisse è
contemporaneamente troppo e troppo poco. Uno stack di 2 MB sarebbe un enorme spreco di memoria
per una piccola goroutine, come quella che si limita ad attendere un gruppo di attesa e a chiudere un
canale. Non è raro che un programma Go crei centinaia di migliaia di goroutine alla volta, cosa che
sarebbe impossibile con stack così grandi. Tuttavia, nonostante le loro dimensioni, gli stack a
dimensione fissa non sono sempre abbastanza grandi per le funzioni più complesse e profondamente
ricorsive. La modifica della dimensione fissa può migliorare l'efficienza dello spazio e consentire la
creazione di un maggior numero di thread, oppure può consentire funzioni più profondamente
ricorsive, ma non può fare entrambe le cose.
Al contrario, una goroutine inizia con un piccolo stack, in genere 2KB. Lo stack di una goroutine, come
quello di un thread del sistema operativo, contiene le variabili locali delle chiamate di funzione attive e
sospese, ma a differenza di un thread del sistema operativo, lo stack di una goroutine non è fisso; cresce e
si riduce secondo le necessità. Il limite di dimensione dello stack di una goroutine può arrivare a 1 GB,
un ordine di grandezza superiore a quello di un tipico stack di un thread a dimensione fissa, anche se
ovviamente poche goroutine ne usano così tanto.
Esercizio 9.4: Costruire una pipeline che colleghi un numero arbitrario di goroutine con canali. Qual è il
numero massimo di stadi della pipeline che è possibile creare senza esaurire la memoria? Quanto tempo
impiega un valore per attraversare l'intera pipeline?

9.8.2. Programmazione goroutine

I thread del sistema operativo sono programmati dal kernel del sistema operativo. Ogni pochi
millisecondi, un timer hardware interviene sul processore e fa sì che venga invocata una funzione del
kernel chiamata scheduler. Questa funzione sospende il thread in esecuzione e ne salva i registri in
memoria, esamina l'elenco dei thread e decide quale deve essere eseguito successivamente, ripristina i
registri di quel thread dalla memoria, quindi riprende l'esecuzione di quel thread. Poiché i thread del
sistema operativo sono programmati dal kernel, il passaggio del controllo da un thread all'altro richiede
un cambio di contesto completo, cioè il salvataggio dello stato di un thread utente in memoria, il
ripristino dello stato di un altro e l'aggiornamento delle strutture dati dello scheduler. Questa operazione
è lenta, a causa della scarsa localizzazione e del numero di accessi alla memoria richiesti, e storicamente è
peggiorata con l'aumento del numero di cicli della CPU necessari per accedere alla memoria.

www.it-ebooks.info
SEZIONE 9.8. GOROUTINE E THREAD 281

Il runtime Go contiene un proprio scheduler che utilizza una tecnica nota come scheduling m:n, in
quanto multiplexa (o schedula) m goroutine su n thread del sistema operativo. Il lavoro dello scheduler
di Go è analogo a quello dello scheduler del kernel, ma si occupa solo delle goroutine di un singolo
programma Go.
A differenza del thread scheduler del sistema operativo, lo scheduler di Go non viene invocato
periodicamente da un timer hardware, ma implicitamente da alcuni costrutti del linguaggio Go. Ad
esempio, quando una goroutine chiama time.Sleep o si blocca in un'operazione di canale o di mutex, lo
scheduler la mette a riposo ed esegue un'altra goroutine finché non è il momento di risvegliare la prima.
Poiché non è necessario passare al contesto del kernel, la riprogrammazione di una goroutine è molto
più economica di quella di un thread.
Esercizio 9.5: Scrivere un programma con due goroutine che inviano messaggi avanti e indietro su due
canali non bufferizzati in modo ping-pong. Quante comunicazioni al secondo può sostenere il
programma?

9.8.3. GOMAXPROCS

Lo scheduler di Go utilizza un parametro chiamato GOMAXPROCS per determinare quanti thread del
sistema operativo possono eseguire attivamente codice Go contemporaneamente. Il suo valore
predefinito è il numero di CPU della macchina, quindi su una macchina con 8 CPU, lo scheduler
pianificherà il codice Go su un massimo di 8 thread OS contemporaneamente. (GOMAXPROCS è l'n nella
schedulazione m:n) Le gorooutine che dormono o sono bloccate in una comunicazione non hanno
bisogno di un thread. Le gorooutine bloccate in I/O o in altre chiamate di sistema o che chiamano
funzioni non Go hanno bisogno di un thread del sistema operativo, ma GOMAXPROCS non deve tenerne
conto.
È possibile controllare esplicitamente questo parametro utilizzando la variabile d'ambiente GOMAXPROCS
o la funzione runtime.GOMAXPROCS. Possiamo vedere l'effetto di GOMAXPROCS su questo piccolo
programma, che stampa un flusso infinito di zeri e uno:
per {
go fmt.Print(0)
fmt.Print(1)
}

$ GOMAXPROCS=1 go run hacker-cliché.go 111111111111111111110000000000000000000011111...

$ GOMAXPROCS=2 go run hacker-cliché.go 010101010101010101011001100101011010010100110...

Nella prima esecuzione, veniva eseguita al massimo una goroutine alla volta. Inizialmente si trattava
della goroutine principale, che stampa gli uno. Dopo un certo periodo di tempo, lo scheduler di Go l'ha
messa a dormire e ha risvegliato la goroutine che stampa gli zeri, dandole un turno di esecuzione sul
thread del sistema operativo. Nella seconda esecuzione, c'erano due thread del sistema operativo
disponibili, quindi entrambe le goroutine sono state eseguite simultaneamente, stampando cifre alla
stessa velocità. Va sottolineato che lo scheduling delle goroutine dipende da molti fattori e che il runtime
è in continua evoluzione, per cui i risultati potrebbero differire da quelli riportati sopra.

www.it-ebooks.info
282 CAPITOLO 9. CONCORRENZA CON VARIABILI CONDIVISE

Esercizio 9.6: Misurare come variano le prestazioni di un programma parallelo legato al calcolo (vedi
Esercizio 8.5) con GOMAXPROCS. Qual è il valore ottimale sul vostro computer? Quante CPU ha il vostro
computer?

9.8.4. Le gorochine non hanno identità

Nella maggior parte dei sistemi operativi e dei linguaggi di programmazione che supportano il
multithreading, il thread corrente ha un'identità distinta che può essere facilmente ottenuta come valore
ordinario, in genere un intero o un puntatore. In questo modo è facile costruire un'astrazione chiamata
thread-local storage, che è essenzialmente una mappa globale con chiave di identità del thread, in modo
che ogni thread possa memorizzare e recuperare valori indipendentemente dagli altri thread.
Le goroutine non hanno una nozione di identità accessibile al programmatore. Questo è stato progettato,
poiché la memorizzazione locale dei thread tende a essere abusata. Ad esempio, in un server web
implementato in un linguaggio con memorizzazione thread-local, è comune che molte funzioni trovino
informazioni sulla richiesta HTTP per conto della quale stanno lavorando, guardando in quella
memorizzazione. Tuttavia, proprio come accade nei programmi che si affidano eccessivamente alle
variabili globali, questo può portare a una malsana "azione a distanza", in cui il comportamento di una
funzione non è determinato solo dai suoi argomenti, ma dall'identità del thread in cui viene eseguita. Di
conseguenza, se l'identità del thread dovesse cambiare - ad esempio, se alcuni thread worker vengono
arruolati per aiutare - la funzione si comporta in modo misterioso.
Go incoraggia uno stile di programmazione più semplice, in cui i parametri che influenzano il
comportamento di una funzione sono espliciti. Questo non solo rende i programmi più facili da leggere,
ma ci permette di assegnare liberamente sottoattività di una data funzione a molte goroutine diverse
senza preoccuparci della loro identità.

Ora avete appreso tutte le caratteristiche del linguaggio necessarie per scrivere programmi Go. Nei
prossimi due capitoli, faremo un passo indietro per esaminare alcune delle pratiche e degli strumenti che
supportano la programmazione in ambiente large: come strutturare un progetto come un insieme di
pacchetti e come ottenere, costruire, testare, fare benchmark, profilare, documentare e condividere tali
pacchetti.

www.it-ebooks.info
10
Pacchetti e strumento Go

Un programma di dimensioni modeste oggi potrebbe contenere 10.000 funzioni. Tuttavia, l'autore deve
pensare solo a poche di esse e progettarne ancora meno, perché la maggior parte è stata scritta da altri e
resa disponibile per il riutilizzo attraverso i pacchetti.
Go è dotato di oltre 100 pacchetti standard che forniscono le basi per la maggior parte delle applicazioni.
La comunità di Go, un fiorente ecosistema di progettazione, condivisione, riutilizzo e miglioramento dei
pacchetti, ne ha pubblicati molti altri, di cui è possibile trovare un indice ricercabile all'indirizzo
http://godoc.org. In questo capitolo mostreremo come utilizzare i pacchetti esistenti e come crearne di
nuovi.
Go è dotato anche dello strumento go, un comando sofisticato ma semplice da usare per gestire gli spazi
di lavoro dei pacchetti Go. Fin dall'inizio del libro, abbiamo mostrato come utilizzare lo strumento go
per scaricare, compilare ed eseguire programmi di esempio. In questo capitolo, esamineremo i concetti
alla base dello strumento e faremo un tour delle sue funzionalità, che includono la stampa di documenti
e l'interrogazione dei metadati relativi ai pacchetti nello spazio di lavoro. Nel prossimo capitolo
esploreremo le sue funzioni di test.

10.1. Introduzione

Lo scopo di qualsiasi sistema di pacchetti è quello di rendere pratica la progettazione e la manutenzione


di programmi di grandi dimensioni, raggruppando funzioni correlate in unità che possono essere
facilmente comprese e modificate, indipendentemente dagli altri pacchetti del programma. Questa
modularità permette ai pacchetti di essere condivisi e riutilizzati da diversi progetti, distribuiti all'interno
di un'organizzazione o resi disponibili al mondo intero.
Ogni pacchetto definisce uno spazio dei nomi distinto che racchiude i suoi identificatori. Ogni nome è
associato a un particolare pacchetto, consentendoci di scegliere nomi brevi e chiari per i tipi, le funzioni
e così via che usiamo più spesso, senza creare conflitti con altre parti del programma.

283

www.it-ebooks.info
284 CAPITOLO 10. PACCHETTI E LO STRUMENTO GO

I pacchetti forniscono anche l'incapsulamento controllando quali nomi sono visibili o esportati al di
fuori del pacchetto. La limitazione della visibilità dei membri del pacchetto nasconde le funzioni e i tipi
di aiuto dietro l'API del pacchetto, consentendo al manutentore del pacchetto di modificare
l'implementazione con la certezza che nessun codice al di fuori del pacchetto sarà influenzato. La
limitazione della visibilità nasconde anche le variabili, in modo che i client possano accedervi e
aggiornarle solo attraverso funzioni esportate che preservano gli invarianti interni o applicano la mutua
esclusione in un programma concorrente.
Quando cambiamo un file, dobbiamo ricompilare il pacchetto del file e potenzialmente tutti i pacchetti
che dipendono da esso. La compilazione di Go è notevolmente più veloce rispetto alla maggior parte
degli altri linguaggi compilati, anche quando si costruisce da zero. La velocità del compilatore è dovuta a
tre ragioni principali. In primo luogo, tutte le importazioni devono essere elencate esplicitamente
all'inizio di ogni file sorgente, in modo che il compilatore non debba leggere ed elaborare un intero file
per determinare le sue dipendenze. In secondo luogo, le dipendenze di un pacchetto formano un grafo
aciclico diretto e, poiché non ci sono cicli, i pacchetti possono essere compilati separatamente e forse in
parallelo. Infine, il file oggetto di un pacchetto Go compilato registra informazioni di esportazione non
solo per il pacchetto stesso, ma anche per le sue dipendenze. Quando compila un pacchetto, il
compilatore deve leggere un file oggetto per ogni importazione, ma non deve guardare oltre questi file.

10.2. Importazione dei percorsi

Ogni pacchetto è identificato da una stringa unica, chiamata percorso di importazione. I percorsi di
importazione sono le stringhe che appaiono nelle dichiarazioni di importazione.
importare (
"fmt" "math/rand"
"encoding/json"

"golang.org/x/net/html"

"github.com/go-sql-driver/mysql"
)

Come abbiamo detto nella Sezione 2.6.1, le specifiche del linguaggio Go non definiscono il significato di
queste stringhe o come determinare il percorso di importazione di un pacchetto, ma lasciano queste
questioni agli strumenti. In questo capitolo, daremo uno sguardo dettagliato a come lo strumento go le
interpreta, dato che è quello che la maggior parte dei programmatori Go usa per costruire, testare e così
via. Esistono però altri strumenti. Ad esempio, i programmatori Go che utilizzano il sistema di
compilazione multilingue interno di Google seguono regole diverse per nominare e localizzare i
pacchetti, specificare i test e così via, che corrispondono maggiormente alle convenzioni di quel sistema.
Per i pacchetti che si intende condividere o pubblicare, i percorsi di importazione devono essere univoci
a livello globale. Per evitare conflitti, i percorsi di importazione di tutti i pacchetti diversi da quelli della
libreria standard dovrebbero iniziare con il nome del dominio Internet dell'organizzazione che possiede
o ospita il pacchetto; questo rende anche possibile trovare i pacchetti. Per esempio, la dichiarazione qui
sopra importa un parser HTML gestito dal team di Go e un popolare driver di database MySQL di terze
parti.

www.it-ebooks.info
SEZIONE 10.4. DICHIARAZIONI DI 285
IMPORTAZIONE

10.3. La dichiarazione del pacchetto

Una dichiarazione di pacchetto è richiesta all'inizio di ogni file sorgente di Go. Il suo scopo principale è
quello di determinare l'identificatore predefinito per quel pacchetto (chiamato nome del pacchetto)
quando viene importato da un altro pacchetto.
Ad esempio, ogni file del pacchetto math/rand inizia con package rand, quindi quando si importa
questo pacchetto, si può accedere ai suoi membri come rand.Int, rand.Float64 e così via.
pacchetto main

import (
"fmt"
"math/rand"
)

func main() {
fmt.Println(rand.Int())
}

Convenzionalmente, il nome del pacchetto è l'ultimo segmento del percorso di importazione; di


conseguenza, due pacchetti possono avere lo stesso nome anche se i loro percorsi di importazione
sono necessariamente diversi. Ad esempio, i pacchetti i cui percorsi di importazione sono math/rand
e crypto/rand hanno entrambi il nome rand. Tra poco vedremo come utilizzarli entrambi nello stesso
programma.
Ci sono tre eccezioni principali alla convenzione dell'"ultimo segmento". La prima è che un pacchetto
che definisce un comando (un programma Go eseguibile) ha sempre il nome main, indipendentemente
dal percorso di importazione del pacchetto. Questo è un segnale per go build (§10.7.3) che deve invocare
il linker per creare un file eseguibile.
La seconda eccezione è che alcuni file della directory possono avere il suffisso _test nel loro nome di
pacchetto se il nome del file termina con _test.go. Una directory di questo tipo può definire due
pacchetti: il solito, più un altro chiamato pacchetto di test esterno. Il suffisso _test segnala a go test
che deve compilare entrambi i pacchetti e indica quali file appartengono a ciascun pacchetto. I pacchetti
di test esterni sono usati per evitare cicli nel grafo delle importazioni derivanti dalle dipendenze del test;
sono trattati in modo più dettagliato nella sezione 11.2.4.
La terza eccezione è che alcuni strumenti per la gestione delle dipendenze aggiungono il suffisso del
numero di versione ai percorsi di importazione dei pacchetti, come "gopkg.in/yaml.v2". Il nome del
pacchetto esclude il suffisso, quindi in questo caso sarebbe solo yaml.

10.4. Importazione di Dichiarazioni

Un file sorgente Go può contenere zero o più dichiarazioni di importazione subito dopo la dichiarazione
del pacchetto e prima della prima dichiarazione di non importazione. Ogni dichiarazione di importazione
può specificare il percorso di importazione di un singolo pacchetto o di più pacchetti in un elenco con
parentesi. Le due forme seguenti sono equivalenti, ma la seconda è più comune.

www.it-ebooks.info
286 CAPITOLO 10. PACCHETTI E LO STRUMENTO GO

importare "fmt"
importare "os"
importare (
"fmt"
"os"
)

I pacchetti importati possono essere raggruppati introducendo righe vuote; tali raggruppamenti di solito
indicano domini diversi. L'ordine non è significativo, ma per convenzione le righe di ogni gruppo sono
ordinate alfabeticamente. (Sia gofmt che goimports raggruppano e ordinano per voi).
importare (
"fmt"
"html/template"
"os"
"golang.org/x/net/html" "golang.org/x/net/ipv4"
)

Se dobbiamo importare due pacchetti che hanno lo stesso nome, come math/rand e crypto/rand, in
un terzo pacchetto, la dichiarazione di importazione deve specificare un nome alternativo per
almeno uno di essi, per evitare un conflitto. Questa operazione si chiama rinominazione
dell'importazione.
importare (
"crypto/rand"
mrand "math/rand" // il nome alternativo mrand evita conflitti
)

Il nome alternativo ha effetto solo sul file di importazione. Altri file, anche quelli dello stesso pacchetto,
possono importare il pacchetto utilizzando il nome predefinito o un nome diverso.
La ridenominazione dell'importazione può essere utile anche quando non ci sono conflitti. Se il nome
del pacchetto importato è ingombrante, come a volte accade per il codice generato automaticamente, un
nome abbreviato può essere più conveniente. Lo stesso nome abbreviato dovrebbe essere usato in modo
coerente per evitare confusione. La scelta di un nome alternativo può aiutare a evitare conflitti con i
nomi di variabili locali comuni. Ad esempio, in un file con molte variabili locali denominate path, si
potrebbe importare il pacchetto standard "path" come pathpkg.
Ogni dichiarazione di importazione stabilisce una dipendenza dal pacchetto corrente al pacchetto
importato. Lo strumento go build segnala un errore se queste dipendenze formano un ciclo.

10.5. Blank Imports

È un errore importare un pacchetto in un file ma non fare riferimento al nome che definisce all'interno
del file stesso. Tuttavia, a volte dobbiamo importare un pacchetto solo per gli effetti collaterali che ne
derivano: la valutazione delle espressioni di inizializzazione delle sue variabili a livello di pacchetto e
l'esecuzione delle sue funzioni di init (§2.6.2). Per evitare l'errore di ''importazione non utilizzata'' che
altrimenti si verificherebbe, dobbiamo usare un'importazione con rinominazione in cui il nome
alternativo è _, l'identificatore vuoto. Come al solito, l'elemento

www.it-ebooks.info
SEZIONE 10.5. IMPORTAZIONI IN 287
BIANCO

L'identificatore vuoto non può mai essere referenziato.


importare _ "image/png" // registrare il decodificatore PNG

Questa è nota come importazione in bianco. Viene spesso utilizzato per implementare un meccanismo a
tempo di compilazione in base al quale il programma principale può abilitare funzionalità opzionali
importando pacchetti aggiuntivi. Prima vedremo come usarlo, poi vedremo come funziona.
Il pacchetto image della libreria standard esporta una funzione Decode che legge i byte da un lettore
io.Reader, scopre quale formato di immagine è stato usato per codificare i dati, invoca il
decodificatore appropriato e restituisce il risultato image.Image. Utilizzando image.Decode, è facile
costruire un semplice convertitore di immagini che legge un'immagine in un formato e la scrive in
un altro:
gopl.io/ch10/jpeg
// Il comando jpeg legge un'immagine PNG dallo standard input
// e la scrive come immagine JPEG sullo standard output. pacchetto
main

importare (
"fmt"
"immagine"
"immagine/jpe
g"
_ "image/png" // registra il decodificatore PNG
"io"
"os"
)

func main() {
if err := toJPEG(os.Stdin, os.Stdout); err := nil {
fmt.Fprintf(os.Stderr, "jpeg: %v\n", err) os.Exit(1)
}
}

func toJPEG(in io.Reader, out io.Writer) error { img, kind,


err := image.Decode(in)
if err != nil {
return err
}
fmt.Fprintln(os.Stderr, "Formato ingresso =", tipo)
return jpeg.Encode(out, img, &jpeg.Options{Quality: 95})
}

Se diamo in pasto l'output di gopl.io/ch3/mandelbrot (§3.3) al programma di conversione, questo


rileva il formato PNG di ingresso e scrive una versione JPEG della Figura 3.3.
$ go build gopl.io/ch3/mandelbrot
$ go build gopl.io/ch10/jpeg
$ ./mandelbrot | ./jpeg >mandelbrot.jpg Formato
di ingresso = png

www.it-ebooks.info
288 CAPITOLO 10. PACCHETTI E LO STRUMENTO GO

Si noti l'importazione vuota di image/png. Senza questa riga, il programma compila e collega come al
solito, ma non è più in grado di riconoscere o decodificare l'input in formato PNG:
$ go build gopl.io/ch10/jpeg
$ ./mandelbrot | ./jpeg >mandelbrot.jpg jpeg:
immagine: formato sconosciuto

Ecco come funziona. La libreria standard fornisce decodificatori per GIF, PNG e JPEG e gli utenti
possono fornirne altri, ma per mantenere gli eseguibili piccoli, i decodificatori non sono inclusi in
un'applicazione a meno che non siano esplicitamente richiesti. La funzione image.Decode consulta
una tabella dei formati supportati. Ogni voce della tabella specifica quattro cose: il nome del formato;
una stringa che è un prefisso di tutte le immagini codificate in questo modo, usata per rilevare la
codifica; una funzione Decode che decodifica un'immagine codificata; e un'altra funzione
DecodeConfig che decodifica solo i metadati dell'immagine, come le dimensioni e lo spazio colore.
Una voce viene aggiunta alla tabella richiamando image.RegisterFormat, in genere dall'inizializzatore
del pacchetto di supporto per ogni formato, come questo di image/png:
pacchetto png // immagine/png
func Decode(r io.Reader) (image.Image, error)
func DecodeConfig(r io.Reader) (image.Config, error)
func init() {
const pngHeader = "\x89PNG\r\n\x1a\n" image.RegisterFormat("png",
pngHeader, Decode, DecodeConfig)
}

L'effetto è che un'applicazione deve solo importare il pacchetto per il formato di cui ha bisogno per far sì
che la funzione image.Decode sia in grado di decodificarlo.
Il pacchetto database/sql utilizza un meccanismo simile per consentire agli utenti di installare solo i
driver di database di cui hanno bisogno. Ad esempio:
importare (
"database/mysql"
_ "github.com/lib/pq" // abilita il supporto per Postgres
_ "github.com/go-sql-driver/mysql" // abilita il supporto per MySQL
)
db, err = sql.Open("postgres", dbname) // OK db, err =
sql.Open("mysql", dbname) // OK
db, err = sql.Open("sqlite3", dbname) // restituisce un errore:
driver sconosciuto "sqlite3"

Esercizio 10.1: Estendere il programma jpeg in modo che converta qualsiasi formato di input supportato
in qualsiasi formato di output, utilizzando image.Decode per rilevare il formato di input e un flag per
selezionare il formato di output.
Esercizio 10.2: Definire una funzione generica di lettura di file di archivio in grado di leggere file ZIP
(archive/zip) e file tar POSIX (archive/tar). Utilizzate un meccanismo di registrazione simile a
quello descritto sopra, in modo che il supporto per ogni formato di file possa essere inserito utilizzando
importazioni vuote.

www.it-ebooks.info
SEZIONE 10.6. PACCHETTI E DENOMINAZIONE 289

10.6. Pacchetti e nomi

In questa sezione verranno forniti alcuni consigli su come seguire le convenzioni distintive di Go per la
denominazione dei pacchetti e dei loro membri.
Quando si crea un pacchetto, il suo nome deve essere breve, ma non così breve da risultare criptico. I
pacchetti più utilizzati nella libreria standard si chiamano bufio, bytes, flag, fmt, http, io, json, os,
sort, sync e time.
Siate descrittivi e non ambigui, ove possibile. Ad esempio, non chiamate un pacchetto di utilità util
quando un nome come imageutil o ioutil è specifico ma comunque conciso. Evitate di scegliere
nomi di pacchetti che sono comunemente usati per variabili locali correlate, altrimenti potreste
costringere i client del pacchetto a usare importazioni di rinominazione, come nel caso del
pacchetto path.
I nomi dei pacchetti di solito assumono la forma singolare. I pacchetti standard bytes, errors e
strings usano il plurale per evitare di nascondere i corrispondenti tipi predichiarati e, nel caso di
go/types, per evitare conflitti con una parola chiave.
Evitare nomi di pacchetti che hanno già altre connotazioni. Per esempio, originariamente abbiamo
usato il nome temp per il pacchetto di conversione della temperatura nella Sezione 2.5, ma non è durato
a lungo. Era una pessima idea perché "temp" è un sinonimo quasi universale di "temporaneo". Abbiamo
avuto un breve periodo con il nome temperature, ma era troppo lungo e non diceva cosa faceva il
pacchetto. Alla fine è diventato tempconv, che è più corto e parallelo a strconv.
Passiamo ora alla denominazione dei membri del pacchetto. Poiché ogni riferimento a un membro di
un altro pacchetto utilizza un identificatore qualificato come fmt.Println, l'onere di descrivere il
membro del pacchetto è sostenuto in egual misura dal nome del pacchetto e dal nome del membro. Non
è necessario menzionare il concetto di formattazione in Println, perché il nome del pacchetto fmt lo fa
già. Quando si progetta un pacchetto, bisogna considerare come le due parti di un identificatore
qualificato lavorano insieme, non il solo nome del membro. Ecco alcuni esempi caratteristici:
bytes.Equal flag.Int http.Get json.Marshal

Possiamo identificare alcuni modelli di denominazione comuni. Il pacchetto strings fornisce una serie
di funzioni indipendenti per la manipolazione delle stringhe:
pacchetto stringhe

func Index(ago, pagliaio stringa) int type

Replacer struct{ /* ... */ }


func NewReplacer(oldnew ...string) *Replacer
type Reader struct{ /* ... */ } func
NewReader(s string) *Reader

La parola stringa non compare in nessuno dei loro nomi. I clienti si riferiscono a loro come
strings.Index, strings.Replacer e così via.
Altri pacchetti che potremmo definire di tipo singolo, come html/template e math/rand, espongono un
tipo di dati principale e i suoi metodi, e spesso una funzione New per creare istanze.

www.it-ebooks.info
290 CAPITOLO 10. PACCHETTI E LO STRUMENTO GO

pacchetto rand // "math/rand"

tipo Rand struct{ /* ... */ } func


New(source Source) *Rand

Questo può portare a ripetizioni, come in template.Template o rand.Rand, motivo per cui i nomi di questo
tipo di pacchetti sono spesso particolarmente brevi.

All'altro estremo, ci sono pacchetti come net/http che hanno molti nomi senza molta struttura,
perché svolgono un compito complicato. Nonostante abbia più di venti tipi e molte più funzioni, i
membri più importanti del pacchetto hanno i nomi più semplici: Get, Post, Handle, Error, Client, Server.

10.7. Lo strumento Go

Il resto di questo capitolo riguarda lo strumento go, utilizzato per scaricare, interrogare, formattare,
costruire, testare e installare pacchetti di codice Go.

Lo strumento go combina le caratteristiche di una serie di strumenti diversi in un unico set di comandi.
È un gestore di pacchetti (analogo ad apt o rpm) che risponde alle domande sul suo inventario di
pacchetti, calcola le loro dipendenze e li scarica da sistemi remoti di controllo della versione. È un
sistema di compilazione che calcola le dipendenze dei file e invoca compilatori, assemblatori e link,
sebbene sia intenzionalmente meno completo del make standard di Unix. È anche un driver di test, come
vedremo nel Capitolo 11.

La sua interfaccia a riga di comando utilizza lo stile "coltellino svizzero", con oltre una dozzina di
comandi secondari, alcuni dei quali già visti, come get, run, build e fmt. Per vedere l'indice della
documentazione incorporata, si può eseguire il comando go help, ma per riferimento, abbiamo
elencato qui di seguito i comandi più comunemente usati:
$ vai
...
compilare compilare i pacchetti e le dipendenze
pulire rimuovere i file oggetto
doc mostra la documentazione del pacchetto o del
simbolo env stampa le informazioni sull'ambiente Go
fmt esegue gofmt sui sorgenti dei pacchetti
ottenere scaricare e installare pacchetti e dipendenze installare
compilare e installare i pacchetti e le dipendenze elencare
elencare i pacchetti
eseguire compilare ed eseguire il programma Go
test versione dei
pacchetti di test stampa la
versione di Go
veterinario eseguire il veterinario di Go Tool sui pacchetti

Per ulteriori informazioni su un comando, utilizzare "go help [comando]".


...

www.it-ebooks.info
SEZIONE 10.7. LO STRUMENTO 291
GO

Per ridurre al minimo la necessità di configurazione, lo strumento go si basa molto sulle convenzioni.
Per esempio, dato il nome di un file sorgente Go, lo strumento è in grado di trovare il pacchetto che lo
contiene, perché ogni directory contiene un singolo pacchetto e il percorso di importazione di un
pacchetto corrisponde alla gerarchia delle directory nell'area di lavoro. Dato il percorso di importazione
di un pacchetto, lo strumento può trovare la directory corrispondente in cui sono memorizzati i file
oggetto. Può anche trovare l'URL del server che ospita il repository del codice sorgente.

10.7.1. Organizzazione dello spazio di lavoro

L'unica configurazione necessaria per la maggior parte degli utenti è la variabile d'ambiente GOPATH, che
indica la radice dello spazio di lavoro. Quando si passa a un altro spazio di lavoro, l'utente aggiorna il
valore di GOPATH. Per esempio, durante il lavoro su questo libro abbiamo impostato GOPATH su
$HOME/gobook:
$ esporta GOPATH=$HOME/gobook
$ vai a prendere gopl.io/...

Dopo aver scaricato tutti i programmi di questo libro con il comando precedente, il vostro spazio di
lavoro conterrà una gerarchia come questa:
GOPATH/
src/
gopl.io/
.git/
ch1/
helloworld/
main.go
dup/
main.go
...
golang.org/x/net/
.git/
html/
parse.go node.go
...
bin/
dup di
helloworld
pkg/
darwin_amd64/
...

GOPATH ha tre sottodirectory. La sottodirectory src contiene il codice sorgente. Ogni pacchetto
risiede in una directory il cui nome relativo a $GOPATH/src è il percorso di importazione del pacchetto,
come ad esempio gopl.io/ch1/helloworld. Si noti che un singolo spazio di lavoro GOPATH contiene più
repository di controllo della versione sotto src, come gopl.io o golang.org. La sottodirectory pkg è
quella in cui gli strumenti di compilazione conservano i pacchetti compilati, mentre la sottodirectory bin
contiene programmi eseguibili come helloworld.

www.it-ebooks.info
292 CAPITOLO 10. PACCHETTI E LO STRUMENTO GO

Una seconda variabile d'ambiente, GOROOT, specifica la directory principale della distribuzione Go, che
fornisce tutti i pacchetti della libreria standard. La struttura delle directory sotto GOROOT assomiglia a
quella di GOPATH, quindi, per esempio, i file sorgenti del pacchetto fmt risiedono nella directory
$GOROOT/src/fmt. Non è mai necessario impostare GOROOT perché, per impostazione predefinita, lo
strumento go utilizza la posizione in cui è stato installato.
Il comando go env stampa i valori effettivi delle variabili d'ambiente rilevanti per la toolchain, compresi i
valori predefiniti per quelle mancanti. GOOS specifica il sistema operativo di destinazione (per esempio,
android, linux, darwin o windows) e GOARCH specifica l'architettura del processore di destinazione, come
amd64, 386 o arm. Sebbene GOPATH sia l'unica variabile da impostare, le altre compaiono occasionalmente
nelle nostre spiegazioni.
$ go env GOPATH="/home/gopher/gobook"
GOROOT="/usr/local/go"
GOARCH="amd64"
GOOS="darwin"
...

10.7.2. Scaricare i pacchetti

Quando si usa lo strumento go, il percorso di importazione di un pacchetto indica non solo dove
trovarlo nell'area di lavoro locale, ma anche dove trovarlo su Internet in modo che go get possa
recuperarlo e aggiornarlo.
Il comando go get può scaricare un singolo pacchetto o un intero sottoalbero o repository usando la
notazione ..., come nella sezione precedente. Lo strumento calcola e scarica anche tutte le
dipendenze dei pacchetti iniziali, motivo per cui il pacchetto golang.org/x/net/html è apparso
nell'area di lavoro nell'esempio precedente.
Una volta che go get ha scaricato i pacchetti, li costruisce e quindi installa le librerie e i comandi.
Vedremo i dettagli nella prossima sezione, ma un esempio mostrerà quanto s i a semplice il
processo. Il primo comando qui di seguito ottiene lo strumento golint, che controlla i problemi di stile
più comuni nel codice sorgente di Go. Il secondo comando esegue golint su gopl.io/ch2/popcount
della Sezione 2.6.2. Il risultato è che abbiamo dimenticato il codice sorgente di Go. Il comando
ci segnala che abbiamo dimenticato di scrivere un commento di tipo doc per il pacchetto:
$ go get github.com/golang/lint/golint
$ $GOPATH/bin/golint gopl.io/ch2/popcount src/gopl.io/ch2/popcount/main.go:1:1:
Il commento al pacchetto deve essere del tipo "Pacchetto popcount ...".

Il comando go get supporta i siti di code-hosting più diffusi, come GitHub, Bitbucket e Launchpad, e può
effettuare le richieste appropriate ai loro sistemi di controllo delle versioni. Per i siti meno conosciuti,
potrebbe essere necessario indicare il protocollo di controllo di versione da usare nel percorso di
importazione, come Git o Mercurial. Eseguire go help importpath per i dettagli.
Le directory che go get crea sono veri e propri client del repository remoto, non solo copie dei file, quindi
è possibile utilizzare i comandi di controllo di versione per vedere un diff delle modifiche locali
apportate o per

www.it-ebooks.info
SEZIONE 10.7. LO STRUMENTO 293
GO

aggiornare a una revisione diversa. Per esempio, la cartella golang.org/x/net è un client Git:
$ cd $GOPATH/src/golang.org/x/net
$ git remote -v
origine https://go.googlesource.com/net (fetch) origine
https://go.googlesource.com/net (push)

Si noti che il nome di dominio apparente nel percorso di importazione del pacchetto, golang.org,
differisce dal nome di dominio effettivo del server Git, go.googlesource.com. Questa è una
caratteristica dello strumento go che permette ai pacchetti di usare un nome di dominio personalizzato
nel loro percorso di importazione, pur essendo ospitati da un servizio generico come
googlesource.com o github.com. Le pagine HTML sotto https://golang.org/x/net/html includono i
metadati mostrati di seguito, che reindirizzano lo strumento go al repository Git presso il sito di
hosting effettivo:
$ go build gopl.io/ch1/fetch
$ ./fetch h t t p s : / / g o l a n g . o r g / x / n e t / h t m l | grep go-import
<meta name="go-import"
content="golang.org/x/net git https://go.googlesource.com/net">.

Se si specifica il flag -u, go get si assicura che tutti i pacchetti visitati, comprese le dipendenze, siano
aggiornati alla loro ultima versione prima di essere compilati e installati. Senza questo flag, i pacchetti
già esistenti localmente non verranno aggiornati.
Il comando go get -u in genere recupera l'ultima versione di ogni pacchetto, il che è comodo quando si
inizia, ma può essere inadeguato per i progetti distribuiti, dove il controllo preciso delle dipendenze è
fondamentale per l'igiene del rilascio. La soluzione abituale a questo problema è il vendor del
codice, cioè la creazione di una copia locale persistente di tutte le dipendenze necessarie e
l'aggiornamento attento e deliberato di questa copia. Prima di Go 1.5, questo richiedeva la modifica dei
percorsi di importazione dei pacchetti, per cui la nostra copia di golang.org/x/net/html sarebbe
diventata gopl.io/vendor/golang.org/x/net/html. Versioni più recenti dello strumento go
supportano direttamente il vendor, anche se non abbiamo spazio per mostrarne i dettagli in questa sede.
Si veda la voce Vendor Directories nell'output del comando go help gopath.
Esercizio 10.3: Utilizzando fetch http://gopl.io/ch1/helloworld?go-get=1, scoprire quale servizio
ospita gli esempi di codice di questo libro. (Le richieste HTTP da go get includono il parametro go-get in
modo che i server possano distinguerle dalle normali richieste del browser).

10.7.3. Pacchetti di costruzione

Il comando go build compila ogni pacchetto di argomenti. Se il pacchetto è una libreria, il risultato viene
scartato; in questo modo si controlla semplicemente che il pacchetto sia privo di errori di compilazione.
Se il pacchetto si chiama main, go build invoca il linker per creare un eseguibile nella directory corrente; il
nome dell'eseguibile viene preso dall'ultimo segmento del percorso di importazione del pacchetto.
Dal momento che ogni directory contiene un pacchetto, ogni programma eseguibile, o comando nella
ter- minologia Unix, richiede una propria directory. Queste directory sono talvolta figlie di una directory
denominata cmd, come il comando golang.org/x/tools/cmd/godoc che serve la documentazione del
pacchetto Go attraverso un'interfaccia web (§10.7.4).

www.it-ebooks.info
294 CAPITOLO 10. PACCHETTI E LO STRUMENTO GO

I pacchetti possono essere specificati dai loro percorsi di importazione, come abbiamo visto sopra,
o da un nome di directory relativo, che deve iniziare con un segmento . o .. anche se normalmente
non è richiesto. Se non viene fornito alcun argomento, viene assunta la directory corrente. Pertanto, i
comandi seguenti costruiscono lo stesso pacchetto, anche se ciascuno scrive l'eseguibile nella
directory in cui viene eseguito go build:
$ cd $GOPATH/src/gopl.io/ch1/helloworld
$ go build

e:
$ cd anywhere
$ go build gopl.io/ch1/helloworld

e:
$ cd $GOPATH
$ go build ./src/gopl.io/ch1/helloworld

ma non:
$ cd $GOPATH
$ go build src/gopl.io/ch1/helloworld
Errore: impossibile trovare il pacchetto "src/gopl.io/ch1/helloworld".

I pacchetti possono anche essere specificati come un elenco di nomi di file, anche se questo tende a
essere usato solo per piccoli programmi e per esperimenti una tantum. Se il nome del pacchetto è
main, il nome dell'eseguibile deriva dal nome di base del primo file .go.
$ cat quoteargs.go
pacchetto main
importare (
"fmt"
"os"
)
func main() {
fmt.Printf("%q\n", os.Args[1:])
}
$ go build quoteargs.go
$ ./quoteargs uno "due tre" quattro cinque ["uno"
"due tre" "quattro cinque"].

Soprattutto per i programmi usa e getta come questo, vogliamo eseguire l'eseguibile non appena lo
abbiamo costruito. Il comando go run combina questi due passaggi:
$ go run quoteargs.go one "two three" four\ five ["one" "two
three" "four five"]

Il primo argomento che non termina con .go viene assunto come l'inizio dell'elenco degli argomenti
dell'eseguibile Go.
Per impostazione predefinita, il comando go build costruisce il pacchetto richiesto e tutte le sue dipendenze,
quindi getta via tutto il codice compilato, tranne l'e v e n t ua l e eseguibile finale. Sia la dipendenza

www.it-ebooks.info
SEZIONE 10.7. LO STRUMENTO 295
GO

L'analisi e la compilazione sono sorprendentemente veloci, ma quando i progetti crescono fino a decine
di pacchetti e centinaia di migliaia di righe di codice, il tempo per ricompilare le dipendenze può
diventare notevole, potenzialmente di diversi secondi, anche quando tali dipendenze non sono cambiate
affatto.
Il comando go install è molto simile a go build, tranne per il fatto che salva il codice compilato per
ogni pacchetto e comando invece di buttarlo via. I pacchetti compilati vengono salvati nella
directory $GOPATH/pkg, corrispondente alla directory src in cui risiede il sorgente, mentre gli
eseguibili dei comandi vengono salvati nella directory $GOPATH/bin. (Molti utenti mettono
$GOPATH/bin nel loro percorso di ricerca degli eseguibili). In seguito, go build e go install non
eseguono il compilatore per quei pacchetti e comandi se non sono stati modificati, rendendo le
compilazioni successive molto più veloci. Per comodità, go build -i installa i pacchetti che sono
dipendenti dal target di compilazione.
Poiché i pacchetti compilati variano a seconda della piattaforma e dell'architettura, go install li
salva in una sottodirectory il cui nome incorpora i valori delle variabili d'ambiente GOOS e GOARCH. Per
esempio, su un Mac il pacchetto golang.org/x/net/html è compilato e installato nel file
golang.org/x/net/html.a sotto $GOPATH/pkg/darwin_amd64.

È semplice effettuare la cross-compilazione di un programma Go, cioè creare un eseguibile destinato a un


sistema operativo o a una CPU diversi. È sufficiente impostare le variabili GOOS o GOARCH durante la
compilazione. Il programma incrociato stampa il sistema operativo e l'architettura per cui è stato
costruito:
gopl.io/ch10/cross
func main() {
fmt.Println(runtime.GOOS, runtime.GOARCH)
}

I seguenti comandi producono rispettivamente eseguibili a 64 e 32 bit:


$ go build gopl.io/ch10/cross
$ ./cross
darwin amd64
$ GOARCH=386 go build gopl.io/ch10/cross
$ ./cross
darwin 386

Alcuni pacchetti possono richiedere la compilazione di versioni diverse del codice per determinate
piattaforme o processori, ad esempio per risolvere problemi di portabilità di basso livello o per fornire
versioni ottimizzate di routine importanti. Se il nome di un file include il nome di un sistema operativo
o di un'architettura di processore, come net_linux.go o asm_amd64.s, lo strumento go compilerà il file
solo quando viene compilato per quel target. Commenti speciali chiamati tag di compilazione
consentono un controllo più preciso. Per esempio, se un file contiene questo commento:
// +costruire linux darwin

prima della dichiarazione del pacchetto (e del suo commento doc), go build lo compilerà solo quando
costruisce per Linux o Mac OS X, e questo commento dice di non compilare mai il file:
// +costruire ignorare

www.it-ebooks.info
296 CAPITOLO 10. PACCHETTI E LO STRUMENTO GO

Per maggiori dettagli, vedere la sezione Vincoli di compilazione della documentazione del pacchetto
go/build:
$ go doc go/build

10.7.4. Documentazione dei pacchetti

Lo stile di Go incoraggia fortemente una buona documentazione delle API dei pacchetti. Ogni
dichiarazione di un membro di un pacchetto esportato e la dichiarazione stessa del pacchetto
dovrebbero essere immediatamente precedute da un commento che ne spieghi lo scopo e l'uso.
I commenti di Go doc sono sempre frasi complete e la prima frase è solitamente un riassunto che inizia
con il nome dichiarato. I parametri delle funzioni e altri identificatori sono menzionati senza virgolette o
marcatori. Per esempio, ecco il commento doc per fmt.Fprintf:
// Fprintf formatta secondo uno specificatore di formato e scrive in w.
// Restituisce il numero di byte scritti e qualsiasi errore di scrittura riscontrato. func
Fprintf(w io.Writer, format string, a ...interface{}) (int, error)

I dettagli della formattazione di Fprintf sono spiegati in un commento doc associato al pacchetto fmt
stesso. Un commento che precede immediatamente la dichiarazione di un pacchetto è considerato il
commento doc dell'intero pacchetto. Deve essercene uno solo, anche se può apparire in qualsiasi file. I
commenti più lunghi possono giustificare un file a sé stante; quello di fmt è di oltre 300 righe. Questo file
è solitamente chiamato doc.go.
Una buona documentazione non deve essere necessariamente estesa e la documentazione non
sostituisce la semplicità. In effetti, le convenzioni di Go favoriscono la brevità e la semplicità nella
documentazione come in tutte le cose, poiché anche la documentazione, come il codice, richiede
manutenzione. Molte dichiarazioni possono essere spiegate con una frase ben formulata e, se il
comportamento è veramente ovvio, non è necessario alcun commento.
In tutto il libro, per motivi di spazio, abbiamo fatto precedere molte dichiarazioni da commenti doc, ma
troverete esempi migliori navigando nella libreria standard. Due strumenti possono aiutarvi a farlo.
Lo strumento go doc stampa la dichiarazione e il commento doc dell'entità specificata sulla riga di
comando, che può essere un pacchetto:
$ tempo di doc.
pacchetto time // importare "time"

Il pacchetto tempo fornisce funzionalità per la misurazione e la visualizzazione del

tempo. const Nanosecondo Durata = 1 ...


func After(d Duration) <-chan Time func
Sleep(d Duration)
func Da(t Tempo) Durata func
Ora() Tempo
type Duration int64 type
Time struct { ... }
...molti altri...

www.it-ebooks.info
SEZIONE 10.7. LO STRUMENTO 297
GO

o un membro del pacchetto:


$ go doc time.Since
func Da(t Tempo) Durata
Since restituisce il tempo trascorso da t. È
l'abbreviazione di time.Now().Sub(t).

o un metodo:
$ go doc time.Duration.Seconds
func (d Durata) Secondi() float64
Secondi restituisce la durata come numero di secondi in virgola mobile.

Lo strumento non ha bisogno di percorsi di importazione completi o di casi di identificazione corretti.


Questo comando stampa la documentazione di (*json.Decoder).Decode dal pacchetto encoding/json:
$ go doc json.decode
func (dec *Decoder) Decodifica(v interfaccia{}) errore
Decode legge il prossimo valore codificato in JSON dal suo ingresso e lo
memorizza nel valore indicato da v.

Il secondo strumento, chiamato confusamente godoc, serve pagine HTML con collegamenti incrociati
che forniscono le stesse informazioni di go doc e molto di più. Il server godoc di
https://golang.org/pkg copre la libreria standard. La Figura 10.1 mostra la documentazione del pacchetto
time e nella Sezione 11.6 vedremo la visualizzazione interattiva di godoc di programmi di esempio. Il
server godoc all'indirizzo https://godoc.org ha un indice ricercabile di migliaia di pacchetti open-
source.

È anche possibile eseguire un'istanza di godoc nel proprio spazio di lavoro, se si desidera sfogliare i
propri pacchetti. Visitate http://localhost:8000/pkg nel vostro browser mentre eseguite questo
comando:
$ godoc -http :8000

I suoi flag -analysis=type e -analysis=pointer aumentano la documentazione e il codice sorgente


con i risultati dell'analisi statica avanzata.

10.7.5. Pacchetti interni

Il pacchetto è il meccanismo più importante per l'incapsulamento nei programmi Go. Gli identificatori
non portati sono visibili solo all'interno dello stesso pacchetto, mentre quelli esportati sono visibili a
tutto il mondo.
A volte, però, sarebbe utile una via di mezzo, un modo per definire identificatori che siano visibili a un
piccolo gruppo di pacchetti affidabili, ma non a tutti. Ad esempio, quando si suddivide un pacchetto di
grandi dimensioni in parti più gestibili, si potrebbe non voler rivelare le interfacce tra queste parti ad
altri pacchetti. Oppure potremmo voler condividere le funzioni di utilità tra diversi pacchetti di un
progetto senza esporle in modo più ampio. O forse vogliamo semplicemente sperimentare un nuovo
pacchetto senza impegnarci prematuramente con la sua API, mettendolo "in prova" con un insieme
limitato di client.

www.it-ebooks.info
298 CAPITOLO 10. PACCHETTI E LO STRUMENTO GO

Figura 10.1. Il pacchetto tempo in godoc.

Per rispondere a queste esigenze, lo strumento go build tratta un pacchetto in modo speciale se il suo
percorso di importazione contiene un segmento di percorso chiamato internal. Tali pacchetti sono
chiamati pacchetti interni. Un pacchetto interno può essere importato solo da un altro pacchetto che
si trova all'interno dell'albero con radice nel genitore della directory interna. Per esempio, dati i
pacchetti sottostanti, net/http/inter- nal/chunked può essere importato da net/http/httputil o
net/http, ma non da net/url. Tuttavia, net/url può importare net/http/httputil.

net/http net/http/internal/chunked
net/http/httputil
rete/url

10.7.6. Interrogazione dei pacchetti

Lo strumento go list riporta informazioni sui pacchetti disponibili. Nella sua f o r m a più semplice, go list
verifica se un pacchetto è presente nell'area di lavoro e, in caso affermativo, stampa il suo percorso di
importazione:
$ go list github.com/go-sql-driver/mysql
github.com/go-sql-driver/mysql

www.it-ebooks.info
SEZIONE 10.7. LO STRUMENTO 299
GO

Un argomento di go list può contenere il carattere jolly ''...'', che corrisponde a qualsiasi sottostringa
del percorso di importazione di un pacchetto. Si può usare per enumerare tutti i pacchetti all'interno di
uno spazio di lavoro Go:

$ go list ...
archivio/tar
archivio/zip
bufio
byte
cmd/addr2line
cmd/api
...molti altri...

o all'interno di un sottoalbero specifico:

$ go list gopl.io/ch3/...
gopl.io/ch3/basename1
gopl.io/ch3/basename2
gopl.io/ch3/comma
gopl.io/ch3/mandelbrot
gopl.io/ch3/netflag
gopl.io/ch3/printints
gopl.io/ch3/surface

o relativi a un particolare argomento:

$ go list ...xml... encoding/xml


gopl.io/ch7/xmlselect

Il comando go list ottiene i metadati completi di ogni pacchetto, non solo il percorso di
importazione, e rende queste informazioni disponibili agli utenti o ad altri strumenti in diversi formati. Il
comando
Il flag -json fa sì che go list stampi l'intero record di ogni pacchetto in f o r m a t o JSON:

$ go list -json hash


{
"Dir": "/home/gopher/go/src/hash",
"ImportPath": "hash",
"Nome": "hash",
"Doc": "Il pacchetto hash fornisce interfacce per le funzioni hash", "Target":
"/home/gopher/go/pkg/darwin_amd64/hash.a",
"Goroot": vero, "Standard":
vero,
"Root": "/home/gopher/go",
"GoFiles": [
"hash.go"
],
"Importazioni"
: [ "io"
],

www.it-ebooks.info
300 CAPITOLO 10. PACCHETTI E LO STRUMENTO GO

"Deps": [
"errori",
"io", "runtime",
"sync",
"sync/atomic",
"unsafe".
]
}

Il flag -f consente di personalizzare il formato di output utilizzando il linguaggio template del


pacchetto text/template (§4.6). Questo comando stampa le dipendenze transitive del pacchetto
strconv, separate da spazi:
$ go list -f '{{join .Deps " "}}' strconv errors math
runtime unicode/utf8 unsafe

e questo comando stampa le importazioni dirette di ogni pacchetto nel sottoalbero compress della libreria
standard:
$ go list -f '{{.ImportPath}} -> {{join .Imports " "}}' compress/... compress/bzip2 -> bufio
io sort
comprimere/flatare -> bufio fmt io math sort strconv
compress/gzip -> bufio compress/flate errors fmt hash hash/crc32 io time compress/lzw ->
bufio errors fmt io
compress/zlib -> bufio compress/flate errori fmt hash hash/adler32 io

Il comando go list è utile sia per le interrogazioni interattive una tantum che per gli script di
automazione di build e test. Lo utilizzeremo di nuovo nella Sezione 11.2.4. Per maggiori informazioni,
compreso l'insieme dei campi disponibili e il loro significato, si veda l'output di go help list.

In questo capitolo abbiamo spiegato tutti i più importanti sottocomandi dello strumento go, tranne uno.
Nel prossimo capitolo vedremo come il comando go test viene utilizzato per testare i programmi Go.
Esercizio 10.4: Costruire uno strumento che riporti l'insieme di tutti i pacchetti dell'area di lavoro che
dipendono transitoriamente dai pacchetti specificati dagli argomenti. Suggerimento: sarà
necessario eseguire go list due volte, una per i pacchetti iniziali e una per tutti i pacchetti. È possibile
analizzare l'output JSON utilizzando il pacchetto encoding/json (§4.5).

www.it-ebooks.info
11
Test
Maurice Wilkes, lo sviluppatore dell'EDSAC, il primo computer a programma memorizzato, ebbe
un'intuizione sorprendente mentre saliva le scale del suo laboratorio nel 1949. Nelle Memorie di un
pioniere del computer, ha ricordato: "Mi resi conto con forza che buona parte del resto della mia vita
sarebbe stata dedicata a trovare errori nei miei programmi". Sicuramente ogni programmatore di un
computer a programma memorizzato da allora può simpatizzare con Wilkes, anche se forse non senza
un po' di imbarazzo per la sua ingenuità nei confronti delle difficoltà di costruzione del software.
Oggi i programmi sono molto più grandi e complessi rispetto ai tempi di Wilkes, e sono stati fatti molti
sforzi per trovare tecniche che rendano gestibile questa complessità. Due tecniche in particolare si
distinguono per la loro efficacia. La prima è la revisione tra pari di routine dei programmi prima del loro
utilizzo. La seconda, oggetto di questo capitolo, è la sperimentazione.
Il testing, con il quale si intende implicitamente il testing automatizzato, è la pratica di scrivere piccoli
programmi che controllano che il codice sotto test (il codice di produzione) si comporti come previsto
per determinati input, che di solito sono scelti con cura per esercitare determinate funzionalità o sono
eseguiti per garantire un'ampia copertura.
Il campo del testing del software è enorme. Il compito di testare occupa tutti i programmatori per una
parte del tempo e alcuni programmatori per tutto il tempo. La letteratura sul testing comprende migliaia
di libri stampati e milioni di parole di blog. In ogni linguaggio di programmazione mainstream, ci sono
decine di pacchetti software destinati alla costruzione di test, alcuni con una grande quantità di teoria, e
il campo sembra attirare più di qualche profeta con un seguito simile a un culto. È quasi sufficiente per
convincere i programmatori che per scrivere test efficaci devono acquisire una serie di competenze
completamente nuove.
L'approccio di Go ai test può sembrare piuttosto low-tech in confronto. Si basa su un comando, go test,
e su un insieme di convenzioni per la scrittura di funzioni di test che go test può eseguire. Questo
meccanismo relativamente leggero è efficace per i test puri e si estende naturalmente ai benchmark e agli
esempi sistematici per la documentazione.

301

www.it-ebooks.info
302 CAPITOLO 11. TEST

In pratica, scrivere codice di test non è molto diverso dallo scrivere il programma originale. Scriviamo
brevi funzioni che si concentrano su una parte del compito. Dobbiamo prestare attenzione alle
condizioni al contorno, pensare alle strutture dei dati e ragionare sui risultati che un calcolo dovrebbe
produrre a partire da input adeguati. Ma questo è lo stesso processo di scrittura del normale codice Go;
non richiede nuove notazioni, convenzioni e strumenti.

11.1. Lo strumento go test

Il sottocomando go test è un driver di test per i pacchetti Go organizzati secondo determinate


convenzioni. In una directory di pacchetti, i file i cui nomi terminano con _test.go non fanno parte
del pacchetto normalmente costruito da go build, ma ne fanno parte quando vengono costruiti da
go test.

All'interno dei file *_test.go vengono trattati in modo particolare tre tipi di funzioni: test, benchmark ed
esempi. Una funzione di test, il cui nome inizia con Test, esercita la logica di un programma per
verificarne il comportamento corretto; go test chiama la funzione di test e ne riporta il risultato,
che può essere PASS o FAIL. Una funzione di benchmark ha un nome che inizia con Benchmark e misura le
prestazioni di alcune operazioni; go test riporta il tempo medio di esecuzione dell'operazione. Una
funzione di esempio, il cui nome inizia con Example, fornisce una documentazione verificata dalla
macchina. I test saranno trattati in dettaglio nella Sezione 11.2, i benchmark nella Sezione 11.4 e gli
esempi nella Sezione 11.6.
Lo strumento go test analizza i file *_test.go alla ricerca di queste funzioni speciali, genera un pacchetto
principale temporaneo che le richiama tutte nel modo corretto, lo costruisce e lo esegue, riporta i risultati
e poi si ripulisce.

11.2. Test Funzioni

Ogni file di test deve importare il pacchetto di test. Le funzioni di test hanno la seguente firma:
func TestName(t *testing.T) {
// ...
}

I nomi delle funzioni di test devono iniziare con Test; il suffisso opzionale Nome deve iniziare con una
lettera maiuscola:
func TestSin(t *testing.T) { /* ... */ } func
TestCos(t *testing.T) { /* ... */ } func
TestLog(t *testing.T) { /* ... */ }

Il parametro t fornisce metodi per segnalare i fallimenti dei test e registrare informazioni
aggiuntive. Definiamo un pacchetto di esempio gopl.io/ch11/word1, contenente una singola funzione
IsPalindrome che segnala se una stringa si legge allo stesso modo in avanti e all'indietro. (Questa
implementazione testa ogni byte due volte se la stringa è palindroma; ci torneremo a breve).

www.it-ebooks.info
SEZIONE 11.2. FUNZIONI DI PROVA 303

gopl.io/ch11/word1
// Il pacchetto word fornisce utilità per i giochi di parole.
pacchetto word

// IsPalindrome indica se s legge allo stesso modo in avanti e all'indietro.


// (Il nostro primo tentativo).
func IsPalindrome(s string) bool { for i
:= range s {
if s[i] != s[len(s)-1-i] { return
false
}
}
restituire vero
}

Nella stessa directory, il file word_test.go contiene due funzioni di test denominate TestPalin-
drome e TestNonPalindrome. Ciascuna di esse verifica che IsPalindrome dia la risposta giusta per
un singolo input e segnala i fallimenti utilizzando t.Error:
pacchetto word

importazione

"testing"

func TestPalindrome(t *testing.T) { if


!IsPalindrome("detartrated") {
t.Error(`IsPalindrome("detartrated") = false`)
}
if !IsPalindrome("kayak") {
t.Error(`IsPalindrome("kayak") = false`)
}
}

func TestNonPalindrome(t *testing.T) { if


IsPalindrome("palindrome") {
t.Error(`IsPalindrome("palindromo") = true`)
}
}

Un comando go test (o go build) senza argomenti di pacchetto opera sul pacchetto nella directory
corrente. Possiamo costruire ed eseguire i test con il seguente comando.
$ cd $GOPATH/src/gopl.io/ch11/word1
$ go test
ok gopl.io/ch11/word1 0 .008s

Soddisfatti, spediamo il programma, ma non appena gli invitati alla festa di lancio sono partiti, iniziano
ad arrivare le segnalazioni di bug. Un utente francese di nome Noelle Eve Elleon si lamenta che IsPalin-
drome non riconosce ''été''. Un altro, dall'America centrale, è deluso perché rifiuta ''Un uomo, un piano,
un canale: Panama". Queste piccole e specifiche segnalazioni di bug si prestano naturalmente a nuovi
casi di test.

www.it-ebooks.info
304 CAPITOLO 11. TEST

func TestFrenchPalindrome(t *testing.T) { if


!IsPalindrome("été") {
t.Error(`IsPalindrome("été") = false`)
}
}

func TestCanalPalindrome(t *testing.T) { input := "Un


uomo, un piano, un canale: Panama" if
!IsPalindrome(input) {
t.Errorf(`IsPalindrome(%q) = false`, input)
}
}

Per evitare di scrivere due volte la lunga stringa di input, si usa Errorf, che fornisce una formattazione del
tipo
Printf.

Una volta aggiunti i due nuovi test, il comando go test fallisce con messaggi di errore informativi.
$ go test
--- FALLIMENTO: TestFrenchPalindrome (0.00s)
word_test.go:28: IsPalindrome("été") = false
--- FALLIMENTO: TestCanalPalindrome (0.00s)
word_test.go:35: IsPalindrome("Un uomo, un piano, un canale: Panama") = false FAIL
FALLIMENTO gopl.io/ch11/word1 0.014s

È buona norma scrivere prima il test e osservare che si verifichi lo stesso errore descritto nella
segnalazione dell'utente. Solo in questo modo possiamo essere sicuri che la soluzione che abbiamo
trovato risolva il problema giusto.
Inoltre, l'esecuzione di go test è di solito più rapida rispetto all'esecuzione manuale dei passaggi descritti
nella segnalazione del bug, consentendoci di iterare più rapidamente. Se la suite di test contiene molti
test lenti, possiamo fare progressi ancora più rapidi se siamo selettivi su quali eseguire.
Il flag -v stampa il nome e il tempo di esecuzione di ogni test del pacchetto:
$ go test -v
=== Eseguire TestPalindrome
--- PASS: TestPalindrome (0.00s)
=== Eseguire TestNonPalindromo
--- PASS: TestNonPalindromo (0.00s)
=== Eseguire TestFrancescoPalindromo
--- FALLIMENTO: TestFrenchPalindrome (0.00s)
word_test.go:28: IsPalindrome("été") = false
=== Eseguire TestCanalePalindromo
--- FALLIMENTO: TestCanalPalindrome (0.00s)
word_test.go:35: IsPalindrome("Un uomo, un piano, un canale: Panama") = false FAIL
stato di uscita 1
FALLIMENTO gopl.io/ch11/word1 0.017s

www.it-ebooks.info
SEZIONE 11.2. FUNZIONI DI PROVA 305

e il flag -run, il cui argomento è un'espressione regolare, fa sì che go test esegua solo i test il cui
nome di funzione corrisponde allo schema:
$ go test -v -run="Francese|Canale"
=== Eseguire TestFrancescoPalindromo
--- FALLIMENTO: TestFrenchPalindrome (0.00s)
word_test.go:28: IsPalindrome("été") = false
=== Eseguire TestCanalePalindromo
--- FALLIMENTO: TestCanalPalindrome (0.00s)
word_test.go:35: IsPalindrome("Un uomo, un piano, un canale: Panama") = false FAIL
stato di uscita 1
FALLIMENTO gopl.io/ch11/word1 0,014s

Naturalmente, una volta che i test selezionati sono passati, dovremmo invocare go test senza flag per
eseguire l'intera suite di test un'ultima volta prima di effettuare il commit della modifica.

Ora il nostro compito è quello di risolvere i bug. Una rapida indagine rivela che la causa del primo bug è
l'uso da parte di IsPalindrome di sequenze di byte, non di sequenze di rune, per cui i caratteri non ASCII
come la é in "été" lo confondono. Il secondo bug deriva dalla mancata considerazione di spazi,
punteggiatura e lettere maiuscole.

Castigati, riscriviamo la funzione con maggiore attenzione:

gopl.io/ch11/word2
// Il pacchetto word fornisce utilità per i giochi di parole.
pacchetto word

importare "unicode"

// IsPalindrome indica se s legge allo stesso modo in avanti e all'indietro.


// Le lettere vengono ignorate, così come le non
lettere. func IsPalindrome(s string) bool {
var lettere []rune for
_, r := range s {
if unicode.IsLetter(r) {
lettere = append(lettere, unicode.ToLower(r))
}
}
per i := intervallo di lettere {
if lettere[i] != lettere[len(lettere)-1-i] { return
false
}
}
restituire vero
}

Scriviamo anche un insieme più completo di casi di test che combina tutti i precedenti e una serie di nuovi
casi in una tabella.

www.it-ebooks.info
306 CAPITOLO 11. TEST

func TestIsPalindrome(t *testing.T) { var


tests = []struct {
stringa di
input vuole
bool
}{
{"", true},
{"a", true},
{"aa", true},
{"ab", false},
{"kayak", true},
{"detartrato", true},
{"Un uomo, un piano, un canale: Panama", true},
{"Ho abitato male, ho vissuto in modo lascivo", è vero,}
{"Ero in grado prima di vedere l'Elba", vero},
{"été", true},
{"Et se resservir, ivresse reste.", true},
{"palindromo", false}, // non palindromo
{"dessert", false}, // semi-palindromo
}
per _, test := gamma di test {
if got := IsPalindrome(test.input); got := test.want { t.Errorf("IsPalindrome(%q) =
%v", test.input, got)
}
}
}

I nostri nuovi test sono stati superati:


$ go test gopl.io/ch11/word2
ok gopl.io/ch11/word2 0.015s

Questo stile di test guidato dalle tabelle è molto comune in Go. È semplice aggiungere nuove voci di
tabella, se necessario, e poiché la logica dell'asserzione non viene duplicata, si può investire di più nella
produzione di un buon messaggio di errore.
L'output di un test che fallisce non include l'intera traccia dello stack al momento della chiamata a
t.Errorf. Né t.Errorf causa un panico o interrompe l'esecuzione del test, a differenza dei fallimenti di
asserzioni in molti framework di test per altri linguaggi. I test sono indipendenti l'uno dall'altro. Se una
voce iniziale della tabella causa il fallimento del test, le voci successive della tabella saranno comunque
controllate e quindi si può venire a conoscenza di più fallimenti durante una singola esecuzione.
Quando è davvero necessario interrompere una funzione di test, magari perché qualche codice di
inizializzazione è fallito o per evitare che un errore già segnalato provochi una confusa cascata di altri,
si usa t.Fatal o t.Fatalf. Queste devono essere chiamate dalla stessa goroutine della funzione Test,
non da un'altra creata durante il test.
I messaggi di fallimento dei test sono solitamente della forma "f(x) = y, vuole z", dove f(x) spiega
l'operazione tentata e il suo input, y è il risultato effettivo e z il risultato atteso. Se necessario, come nel
nostro esempio di palindromo, per la parte f(x) viene utilizzata la sintassi di Go. La visualizzazione di
x è particolarmente importante in un test guidato da tabelle, poiché una data asserzione viene
eseguita molte volte.

www.it-ebooks.info
SEZIONE 11.2. FUNZIONI DI PROVA 307

volte con valori diversi. Evitate il boilerplate e le informazioni ridondanti. Quando si testa una funzione
booleana come IsPalindrome, omettere la parte "want z" perché non aggiunge informazioni. Se x, y o z sono
lunghi, stampare un riassunto conciso delle parti rilevanti. L'autore di un test deve cercare di aiutare il
programmatore che deve diagnosticare il fallimento di un test.

Esercizio 11.1: Scrivete i test per il programma charcount della Sezione 4.3.

Esercizio 11.2: Scrivere un insieme di test per IntSet (§6.5) che verifichi che il suo comportamento dopo
ogni operazione sia equivalente a quello di un insieme basato su mappe incorporate. Conservate la
vostra implementazione per il benchmarking nell'Esercizio 11.7.

11.2.1. Test randomizzati

I test guidati da tabelle sono comodi per verificare che una funzione funzioni su input accuratamente
selezionati per esercitare casi interessanti nella logica. Un altro approccio, i test randomizzati, esplora
una gamma più ampia di input costruendoli in modo casuale.

Come facciamo a sapere quale output aspettarci dalla nostra funzione, dato un input casuale? Esistono
due strategie. La prima consiste nello scrivere un'implementazione alternativa della funzione che utilizzi
un algoritmo meno efficiente ma più semplice e chiaro, e verificare che entrambe le implementazioni
diano lo stesso risultato. La seconda consiste nel creare valori di input secondo uno schema, in modo da
sapere quale output aspettarsi.

L'esempio seguente utilizza il secondo approccio: la funzione randomPalindrome genera parole che sono
note come palindromi per costruzione.

importare "math/rand"

// randomPalindrome restituisce un palindromo la cui lunghezza e contenuto


// sono derivati dal generatore di numeri pseudo-casuali rng. func
randomPalindrome(rng *rand.Rand) string {
n := rng.Intn(25) // lunghezza casuale fino a 24 rune :=
make([]rune, n)
per i := 0; i < (n+1)/2; i++ {
r := runa(rng.Intn(0x1000)) // runa casuale fino a '\u0999' rune[i] = r
rune[n-1-i] = r
}
restituire stringa(rune)
}

func TestRandomPalindromes(t *testing.T) {


// Inizializza un generatore di numeri pseudo-casuali. seed
:= time.Now().UTC().UnixNano() t.Logf("Random seed: %d",
seed)
rng := rand.New(rand.NewSource(seed))

www.it-ebooks.info
308 CAPITOLO 11. TEST

per i := 0; i < 1000; i++ {


p := randomPalindrome(rng) if
!IsPalindrome(p) {
t.Errorf("IsPalindrome(%q) = false", p)
}
}
}

Poiché i test randomizzati non sono deterministici, è fondamentale che il log del test fallito registri
informazioni sufficienti per riprodurre il fallimento. Nel nostro esempio, l'input p a IsPalindrome ci
dice tutto ciò che dobbiamo sapere, ma per le funzioni che accettano input più complessi, può essere più
semplice registrare il seme del generatore di numeri pseudocasuali (come abbiamo fatto sopra)
piuttosto che scaricare l'intera struttura dei dati di input. Armati del valore del seme, possiamo
facilmente modificare il test per riprodurre il fallimento in modo deterministico.

Utilizzando l'ora corrente come fonte di casualità, il test esplorerà nuovi input ogni volta che viene
eseguito, per tutto il corso della sua vita. Questo è particolarmente utile se il progetto utilizza un sistema
automatizzato per eseguire periodicamente tutti i test.

Esercizio 11.3: TestRandomPalindromes verifica solo i palindromi. Scrivete un test randomizzato che
generi e verifichi i non palindromi.

Esercizio 11.4: Modificare randomPalindrome per esercitare la gestione di IsPalindrome su punteggiatura e


spazi.

11.2.2. Verifica di un comando

Lo strumento go test è utile per testare i pacchetti di librerie, ma con un piccolo sforzo possiamo usarlo
anche per testare i comandi. Un pacchetto chiamato main produce normalmente un programma
eseguibile, ma può essere importato anche come libreria.

Scriviamo un test per il programma echo della Sezione 2.3.2. Abbiamo diviso il programma in due
funzioni: echo svolge il lavoro vero e proprio, mentre main analizza e legge i valori dei flag e riporta gli
eventuali errori restituiti da echo.

gopl.io/ch11/echo
// Echo stampa gli argomenti della riga di comando.
pacchetto main

importare (
"bandiera"
"fmt"
"io"
"os"
"stringhe"
)

www.it-ebooks.info
SEZIONE 11.2. FUNZIONI DI PROVA 309

var (
n = flag.Bool("n", false, "omettere la linea di
separazione") s = flag.String("s", " ", "separatore")
)

var out io.Writer = os.Stdout // modificato durante i test func

main() {
flag.Parse()
if err := echo(!*n, *s, flag.Args()); err := nil {
fmt.Fprintf(os.Stderr, "echo: %v\n", err) os.Exit(1)
}
}

func echo(newline bool, sep string, args []string) error { fmt.Fprint(out,


strings.Join(args, sep))
se newline {
fmt.Fprintln(out)
}
restituire nil
}

Dal test, chiameremo echo con una varietà di argomenti e impostazioni di flag e verificheremo che
stampi l'output corretto in ogni caso, quindi abbiamo aggiunto dei parametri a echo per ridurre la sua
dipendenza dalle variabili globali. Detto questo, abbiamo anche introdotto un'altra variabile globale, out,
l'io.Writer su cui verrà scritto il risultato. Facendo in modo che echo scriva attraverso questa variabile e
non direttamente su os.Stdout, i test possono sostituire un'implementazione diversa di Writer che registra
ciò che è stato scritto per un controllo successivo. Ecco il test, nel file echo_test.go:
pacchetto main

import (
"byte"
"fmt"
"testing"
)

func TestEcho(t *testing.T) { var


tests = []struct { newline bool
sep stringa
argomenti []stringa
desiderare stringa
}{
{true, "", []string{}, "\n"},
{false, "", []string{}, ""},
{true, "\t", []stringa{"uno", "due", "tre"}, "uno, due, tre" },
{true, ",", []string{"a", "b", "c"}, "a,b,c\n"},
{false, ":", []stringa{"1", "2", "3"}, "1:2:3"},
}

www.it-ebooks.info
310 CAPITOLO 11. TEST

per _, test := gamma di test {


descr := fmt.Sprintf("echo(%v, %q, %q)", test.newline,
test.sep, test.args)
out = new(bytes.Buffer) // output catturato
if err := echo(test.newline, test.sep, test.args); err := nil { t.Errorf("%s failed:
%v", descr, err)
continuare
}
got := out.(*bytes.Buffer).String() if
got := test.want {
t.Errorf("%s = %q, vuole %q", descr, got, test.want)
}
}
}

Si noti che il codice di test si trova nello stesso pacchetto del codice di produzione. Sebbene il nome del
pacchetto sia main e definisca una funzione principale, durante i test questo pacchetto agisce come una
libreria che espone la funzione TestEcho al driver di test; la sua funzione principale viene ignorata.
Organizzando il test come una tabella, possiamo facilmente aggiungere nuovi casi di test. Vediamo cosa
succede quando il test fallisce, aggiungendo questa riga alla tabella:
{true, ",", []string{"a", "b", "c"}, "a b c\n"}, // NOTA: aspettativa sbagliata!

vai alle stampe di prova


$ go test gopl.io/ch11/echo
--- FALLIMENTO: TestEcho (0.00s)
echo_test.go:31: echo(true, ",", ["a" "b" "c"]) = "a,b,c", vuole "a b c\n" FAIL
FALLIMENTO gopl.io/ch11/echo 0.006s

Il messaggio di errore descrive l'operazione tentata (usando una sintassi simile a quella di Go), il
comportamento effettivo e quello atteso, in quest'ordine. Con un messaggio di errore informativo come
questo, si può avere un'idea abbastanza precisa della causa principale prima ancora di aver individuato il
codice sorgente del test.
È importante che il codice da testare non chiami log.Fatal o os.Exit, poiché queste funzioni fermano il
processo; la chiamata di queste funzioni deve essere considerata un diritto esclusivo di main. Se accade
qualcosa di totalmente inaspettato e una funzione va in panico, il driver di test si riprenderà, anche se il
test sarà ovviamente considerato un fallimento. Gli errori attesi, come quelli derivanti da un input errato
dell'utente, da file mancanti o da una configurazione non corretta, devono essere segnalati restituendo
un valore di errore non nullo. Fortunatamente (anche se sfortunatamente come illustrazione), il nostro
esempio di echo è così semplice che non restituirà mai un errore non nullo.

11.2.3. Test White-Box

Un modo per classificare i test è il livello di conoscenza che richiedono del funzionamento interno del
pacchetto da testare. Un test black-box non presuppone nulla del pacchetto se non

www.it-ebooks.info
SEZIONE 11.2. FUNZIONI DI PROVA 311

Ciò che è esposto dalla sua API e specificato dalla sua documentazione; gli interni del pacchetto sono
opachi. Al contrario, un test white-box ha un accesso privilegiato alle funzioni e alle strutture dati interne
del pacchetto e può fare osservazioni e modifiche che un normale client non può fare. Per esempio, un
test white-box può verificare che gli invarianti dei tipi di dati del pacchetto siano mantenuti dopo ogni
operazione. (Il nome white box è tradizionale, ma clear box sarebbe più preciso).

I due approcci sono complementari. I test black-box sono solitamente più robusti e richiedono meno
aggiornamenti con l'evoluzione del software. Inoltre, aiutano l'autore del test a immedesimarsi nel
cliente del pacchetto e possono rivelare difetti nella progettazione dell'API. Al contrario, i test white-box
possono fornire una copertura più dettagliata delle parti più difficili dell'implementazione.

Abbiamo già visto esempi di entrambi i tipi. TestIsPalindrome chiama solo la funzione esportata
IsPalindrome ed è quindi un test black-box. TestEcho chiama la funzione echo e aggiorna la variabile
globale out, entrambe non esportate, il che lo rende un test white-box.

Durante lo sviluppo di TestEcho, abbiamo modificato la funzione echo per utilizzare la variabile a livello
di pacchetto out quando scrive il suo output, in modo che il test possa sostituire l'output standard con
un'implementazione alter- nativa che registra i dati per un controllo successivo. Con la stessa tecnica,
possiamo sostituire altre parti del codice di produzione con implementazioni ''false'' facili da testare. Il
vantaggio delle implementazioni false è che possono essere più semplici da configurare, più prevedibili,
più affidabili e più facili da osservare. Possono anche evitare effetti collaterali indesiderati, come
l'aggiornamento di un database di produzione o l'addebito di una carta di credito.

Il codice sottostante mostra la logica di controllo delle quote in un servizio web che fornisce agli utenti
uno spazio di archiviazione in rete. Quando gli utenti superano il 90% della loro quota, il sistema invia
loro un'e-mail di avviso.

gopl.io/ch11/storage1
stoccaggio dei pacchi

importare (
"fmt"
"log"
"net/smtp"
)

func bytesInUse(username string) int64 { return 0 /* ... */ }

// Configurazione del mittente e-mail.


// NOTA: non mettere mai le password nel codice
sorgente! const sender = "[email protected]"
const password = "correcthorsebatterystaple" const
hostname = "smtp.example.com"

const template = `Avviso: si stanno utilizzando %d byte di memoria,


%d%% della tua quota.`

www.it-ebooks.info
312 CAPITOLO 11. TEST

func CheckQuota(username string) { used


:= bytesInUse(username) const quota =
1000000000 // 1GB percent := 100 *
used / quota if percent < 90 {
ritorno // OK
}
msg := fmt.Sprintf(template, used, percent)
auth := smtp.PlainAuth("", mittente, password, hostname) err :=
smtp.SendMail(hostname+":587", auth, mittente,
[]string{username}, []byte(msg)) if err
!= nil {
log.Printf("smtp.SendMail(%s) failed: %s", username, err)
}
}

Vorremmo testarlo, ma non vogliamo che il test invii e-mail reali. Per questo motivo, spostiamo la logica
delle e-mail in una funzione propria e la memorizziamo in una variabile di livello pacchetto non
esportata, notifyUser.
gopl.io/ch11/storage2
var notifyUser = func(username, msg string) {
auth := smtp.PlainAuth("", mittente, password, hostname) err :=
smtp.SendMail(hostname+":587", auth, mittente,
[]string{username}, []byte(msg)) if err
!= nil {
log.Printf("smtp.SendEmail(%s) failed: %s", username, err)
}
}

func CheckQuota(username string) { used


:= bytesInUse(username) const quota =
1000000000 // 1GB percent := 100 *
used / quota if percent < 90 {
ritorno // OK
}
msg := fmt.Sprintf(template, used, percent)
notifyUser(username, msg)
}

Possiamo ora scrivere un test che sostituisce un semplice meccanismo di notifica fittizio all'invio di una
vera e-mail. Questo registra l'utente notificato e il contenuto del messaggio.

pacchetto storage

import (
"stringhe"
"test"
)

www.it-ebooks.info
SEZIONE 11.2. FUNZIONI DI PROVA 313

func TestCheckQuotaNotifiesUser(t *testing.T) { var


notifiedUser, notifiedMsg string notifyUser =
func(user, msg string) {
notifiedUser, notifiedMsg = utente, msg
}
// ...simulare una condizione di utilizzo di 980 MB...
const utente = "[email protected]"
ControllaQuota(utente)
if notifiedUser == "" && notifiedMsg == "" {
t.Fatalf("notifyUser non chiamato")
}
if notifiedUser != utente {
t.Errorf("utente sbagliato (%s) notificato, vuole %s",
notifiedUser, user)
}
const wantSubstring = " 98% della tua quota"
if !strings.Contains(notifiedMsg, wantSubstring) { t.Errorf("messaggio di
notifica inatteso <<%s>>, "+
"vuole la sottostringa %q", notifiedMsg, wantSubstring)
}
}

C'è un problema: dopo il ritorno di questa funzione di test, CheckQuota non funziona più come dovrebbe,
perché utilizza ancora l'implementazione falsa di notifyUsers del test. (Dobbiamo modificare il test per
ripristinare il valore precedente, in modo che i test successivi non ne osservino l'effetto, e dobbiamo farlo
in tutti i percorsi di esecuzione, compresi i fallimenti e i panici dei test. Questo suggerisce naturalmente
di rinviare.
func TestCheckQuotaNotifiesUser(t *testing.T) {
// Salva e ripristina il notifyUser originale. saved
:= notifyUser
defer func() { notifyUser = saved }()
// Installare il falso notifyUser del test. var
notifiedUser, notifiedMsg string notifyUser =
func(user, msg string) {
notifiedUser, notifiedMsg = utente, msg
}
// ...resto del test...
}

Questo schema può essere usato per salvare e ripristinare temporaneamente tutti i tipi di variabili
globali, compresi i flag della riga di comando, le opzioni di debug e i parametri delle prestazioni; per
installare e rimuovere i ganci che fanno sì che il codice di produzione richiami del codice di test quando
accade qualcosa di interessante; e per indurre il codice di produzione in stati rari ma importanti, come
timeout, errori e persino interleavings specifici di attività concorrenti.
L'uso di variabili globali in questo modo è sicuro solo perché go test non esegue normalmente più test in
contemporanea.

www.it-ebooks.info
314 CAPITOLO 11. TEST

11.2.4. Pacchetti di test esterni

Consideriamo i pacchetti net/url, che fornisce un parser di URL, e net/http, che fornisce un server web
e una libreria client HTTP. Come ci si potrebbe aspettare, il livello superiore net/http dipende dal livello
inferiore net/url. Tuttavia, uno dei test di net/url è un esempio c h e dimostra l'interazione tra gli URL e
la libreria client HTTP. In altre parole, un test del pacchetto di livello inferiore importa il pacchetto di
livello superiore.

Figura 11.1. Un test di net/url dipende da net/http.


Dichiarare questa funzione di test nel pacchetto net/url creerebbe un ciclo nel grafo di importazione dei
pacchetti, come rappresentato dalla freccia verso l'alto nella Figura 11.1, ma come abbiamo spiegato
nella Sezione 10.1, le specifiche di Go vietano i cicli di importazione.
Si risolve il problema dichiarando la funzione di test in un pacchetto di test esterno, cioè in un file nella
cartella net/url la cui dichiarazione di pacchetto recita package url_test. Il suffisso aggiuntivo _test
segnala a go test di creare un pacchetto aggiuntivo contenente solo questi file e di eseguire i suoi test.
Può essere utile pensare a questo pacchetto di test esterno come se avesse il percorso di importazione
net/url_test, ma non può essere importato con questo o con qualsiasi altro nome.

Poiché i test esterni vivono in un pacchetto separato, possono importare pacchetti di aiuto che
dipendono anche dal pacchetto da testare; un test all'interno del pacchetto non può farlo. In termini di
livelli di progettazione, il pacchetto di test esterno si trova logicamente più in alto rispetto a entrambi i
pacchetti da cui dipende, come illustrato nella Figura 11.2.

Figura 11.2. I pacchetti di test esterni interrompono i cicli di dipendenza.


Evitando i cicli di importazione, i pacchetti di test esterni consentono ai test, soprattutto a quelli di
integrazione (che verificano l'interazione di più componenti), di importare liberamente altri pacchetti,
esattamente come farebbe un'applicazione.

www.it-ebooks.info
SEZIONE 11.2. FUNZIONI DI PROVA 315

Possiamo usare lo strumento go list per riassumere quali file sorgente Go in una directory di un
pacchetto sono codice di produzione, test all'interno del pacchetto e test esterni. Utilizzeremo il
pacchetto fmt come esempio. GoFiles è l'elenco dei file che contengono il codice di produzione; questi
sono i file che go build includerà nell'applicazione:
$ go list -f={.GoFiles}} fmt [doc.go
format.go print.go scan.go]

TestGoFiles è l'elenco dei file che appartengono anch'essi al pacchetto fmt, ma questi file, i cui
nomi terminano tutti con _test.go, vengono inclusi solo durante la costruzione dei test:
$ go list -f={.TestGoFiles}} fmt [export_test.go]

I test del pacchetto di solito risiedono in questi file, anche se insolitamente fmt non ne ha nessuno;
spiegheremo lo scopo di export_test.go tra poco.

XTestGoFiles è l'elenco dei file che costituiscono il pacchetto di test esterno, fmt_test, quindi
questi file devono importare il pacchetto fmt per poterlo usare. Anche in questo caso, sono inclusi solo
durante il test:
$ go list -f={.XTestGoFiles}} fmt [fmt_test.go
scan_test.go stringer_test.go]

A volte un pacchetto di test esterno può avere bisogno di un accesso privilegiato agli interni del
pacchetto in esame, se ad esempio un test white-box deve vivere in un pacchetto separato per evitare un
ciclo di importazione. In questi casi, si ricorre a un trucco: si aggiungono dichiarazioni a un file
_test.go interno al pacchetto per esporre gli interni necessari al test esterno. Questo file offre al test
una ''porta di servizio'' al pacchetto. Se il file sorgente esiste solo per questo scopo e non contiene test,
viene spesso chiamato export_test.go.

Per esempio, l'implementazione del pacchetto fmt ha bisogno della funzionalità di unicode.Is- Space
come parte di fmt.Scanf. Per evitare di creare una dipendenza indesiderata, fmt non importa il
pacchetto unicode e le sue grandi tabelle di dati; contiene invece un'implementazione più semplice,
che chiama isSpace.

Per garantire che i comportamenti di fmt.isSpace e unicode.IsSpace non si allontanino, fmt


contiene prudentemente un test. È un test esterno e quindi non può accedere direttamente a isSpace,
quindi fmt apre una porta sul retro dichiarando una variabile esportata che contiene la funzione interna
isSpace. Questo è l'intero file export_test.go del pacchetto fmt.

pacchetto fmt

var IsSpace = isSpace

Questo file di test non definisce alcun test; si limita a dichiarare il simbolo esportato fmt.IsSpace per l'uso
da parte del test esterno. Questo trucco può essere usato anche quando un test esterno ha bisogno di
usare alcune tecniche di test white-box.

www.it-ebooks.info
316 CAPITOLO 11. TEST

11.2.5. Scrivere test efficaci

Molti neofiti di Go sono sorpresi dal minimalismo del suo framework di test. I framework di altri
linguaggi forniscono meccanismi per identificare le funzioni di test (spesso usando la riflessione o i
metadati), ganci per eseguire operazioni di ''setup'' e ''teardown'' prima e dopo l'esecuzione dei test, e
librerie di funzioni di utilità per asserire predicati comuni, confrontare valori, formattare messaggi
di errore e interrompere un test fallito (spesso usando eccezioni). Sebbene questi meccanismi
possano rendere i test molto concisi, i test risultanti spesso sembrano scritti in una lingua straniera.
Inoltre, anche se possono riportare correttamente PASS o FAIL, il loro modo di procedere può essere
poco amichevole per lo sfortunato manutentore, con messaggi di fallimento criptici come "assert: 0 ==
1" o pagine su pagine di stack trace.
L'atteggiamento di Go nei confronti dei test è in netto contrasto. Si aspetta che gli autori dei test facciano
la maggior parte del lavoro da soli, definendo funzioni per evitare ripetizioni, proprio come farebbero
per i programmi ordinari. Il processo di testing non è un processo di compilazione routinaria di moduli;
un test ha anche un'interfaccia utente, anche se gli unici utenti sono i suoi manutentori. Un buon test
non esplode in caso di fallimento, ma fornisce una descrizione chiara e concisa del sintomo del problema
e forse di altri fatti rilevanti del contesto. Idealmente, il manutentore non dovrebbe aver bisogno di
leggere il codice sorgente per decifrare il fallimento di un test. Un buon test non dovrebbe arrendersi
dopo un solo fallimento, ma dovrebbe cercare di segnalare diversi errori in una singola esecuzione,
poiché lo schema dei fallimenti può essere esso stesso rivelatore.
La funzione di asserzione sottostante confronta due valori, costruisce un messaggio di errore generico e
arresta il programma. È facile da usare ed è corretta, ma quando fallisce, il messaggio di errore è quasi
inutile. Non risolve il difficile problema di fornire una buona interfaccia utente.
importare (
"fmt"
"stringhe"
"test"
)
// Una povera funzione di
asserzione. func assertEqual(x, y
int) {
se x != y {
panic(fmt.Sprintf("%d != %d", x, y))
}
}
func TestSplit(t *testing.T) {
parole := stringhe.Split("a:b:c", ":")
assertEqual(len(parole), 3)
// ...
}

In questo senso, le funzioni di asserzione soffrono di un'astrazione prematura: trattando il fallimento di


questo particolare test come una semplice differenza di due numeri interi, perdiamo l'opportunità di
fornire un contesto significativo. Possiamo fornire un messaggio migliore partendo dai dettagli concreti,
come nell'esempio seguente. Solo quando emergono schemi ripetitivi in una determinata suite di test è il
momento di introdurre astrazioni.

www.it-ebooks.info
SEZIONE 11.2. FUNZIONI DI PROVA 317

func TestSplit(t *testing.T) { s,


sep := "a:b:c", ":"
parole := stringhe.Split(s, sep)
if got, want := len(words), 3; got := want { t.Errorf("Split(%q, %q)
ha restituito %d parole, want %d",
s, sep, got, want)
}
// ...
}

Ora il test riporta la funzione che è stata chiamata, i suoi input e il significato del risultato; identifica
esplicitamente il valore effettivo e l'aspettativa; e continua a essere eseguito anche se questa asserzione
dovesse fallire. Una volta scritto un test di questo tipo, il passo successivo più naturale spesso non è
quello di definire una funzione che sostituisca l'intera istruzione if, ma di eseguire il test in un
ciclo in cui s, sep e want variano, come nel caso del test table-driven di IsPalindrome.
L'esempio precedente non aveva bisogno di funzioni di utilità, ma questo non deve impedirci di
introdurre funzioni che ci aiutano a semplificare il codice. (Vedremo una di queste funzioni di utilità,
reflect.DeepEqual, nella Sezione 13.3). La chiave per un buon test è iniziare con
l'implementazione del comportamento concreto che si desidera e solo successivamente utilizzare le
funzioni per semplificare il codice ed eliminare le ripetizioni. È raro che i risultati migliori si ottengano
partendo da una libreria di funzioni di test astratte e generiche.
Esercizio 11.5: Estendere TestSplit per utilizzare una tabella di input e output previsti.

11.2.6. Evitare i test fragili

Un'applicazione che fallisce spesso quando incontra input nuovi ma validi viene definita buggy; un test
che fallisce spuriosamente quando viene apportata una modifica valida al programma viene definito
brittle. Proprio come un programma buggato frustra i suoi utenti, un test fragile esaspera i suoi
manutentori. I test più fragili, che falliscono per quasi tutte le modifiche al codice di produzione, buone o
cattive che siano, sono talvolta chiamati test di rilevamento delle modifiche o dello status quo, e il tempo
speso per gestirli può esaurire rapidamente qualsiasi beneficio che sembravano fornire.
Quando una funzione sottoposta a test produce un output complesso, come una lunga stringa,
un'elaborata struttura di dati o un file, si è tentati di verificare che l'output sia esattamente uguale a
qualche valore ''aureo'' previsto al momento della stesura del test. Ma con l'evoluzione del programma, è
probabile che alcune parti dell'output cambino, probabilmente in senso positivo, ma comunque
cambiano. E non si tratta solo dell'output; le funzioni con input complessi spesso si interrompono
perché l'input usato in un test non è più valido.
Il modo più semplice per evitare test fragili è controllare solo le proprietà che interessano. Verificate le
interfacce più semplici e stabili del vostro programma piuttosto che le sue funzioni interne. Siate selettivi
nelle asserzioni. Ad esempio, non verificate le corrispondenze esatte tra le stringhe, ma cercate le
sottostringhe rilevanti che rimarranno invariate durante l'evoluzione del programma. Spesso vale la pena
scrivere una funzione sostanziale per distillare un output complesso fino alla sua essenza, in modo che le
asserzioni siano affidabili. Anche se questo può sembrare un grande sforzo iniziale, può essere ripagato
rapidamente in termini di tempo che altrimenti verrebbe speso per correggere i test che falliscono in
modo spurio.

www.it-ebooks.info
318 CAPITOLO 11. TEST

11.3. Copertura

Per sua natura, il test non è mai completo. Come disse l'influente informatico Edsger Dijkstra: "I test
mostrano la presenza, non l'assenza di bug". Nessuna quantità di test potrà mai dimostrare che un
pacchetto è privo di bug. Al massimo, aumentano la fiducia che il pacchetto funzioni bene in un'ampia
gamma di scenari importanti.

Il grado in cui una suite di test esercita il pacchetto in esame è chiamato copertura del test. La copertura
non può essere quantificata direttamente - le dinamiche di tutti i programmi, tranne quelli più banali,
sono al di là di una misurazione precisa - ma ci sono delle euristiche che possono aiutarci a dirigere i
nostri sforzi di test dove è più probabile che siano utili.

La copertura degli statement è la più semplice e la più utilizzata di queste euristiche. La copertura degli
statement di una suite di test è la frazione di istruzioni del sorgente che vengono eseguite almeno una
volta durante il test. In questa sezione, utilizzeremo lo strumento di copertura di Go, integrato in
go test, per misurare la copertura degli statement e aiutare a identificare le lacune più evidenti nei test.

Il codice che segue è un test guidato da tabelle per il valutatore di espressioni che abbiamo costruito nel
capitolo 7:

gopl.io/ch7/eval
func TestCoverage(t *testing.T) { var
tests = []struct {
stringa di
input env
Env
want string // errore atteso da Parse/Check o risultato da Eval
}{
{"x % 2", nil, "inaspettato '%'"},
{"!true", nil, "unexpected '!'"},
{"log(10)", nil, `funzione sconosciuta "log"`},
{"sqrt(1, 2)", nil, "la chiamata a sqrt ha 2 argomenti, ne vuole 1"},
{"sqrt(A / pi)", Env{"A": 87616, "pi": math.Pi}, "167"},
{"pow(x, 3) + pow(y, 3)", Env{"x": 9, "y": 10}, "1729"},
{"5 / 9 * (F - 32)", Env{"F": -40}, "-40"},
}

for _, test := range test { expr, err :=


Parse(test.input) if err == nil {
err = expr.Check(map[Var]bool{})
}
if err != nil {
if err.Error() != test.want {
t.Errorf("%s: ottenuto %q, vuole %q", test.input, err, test.want)
}
continuare
}

www.it-ebooks.info
SEZIONE 11.3. COPERTURA 319

got := fmt.Sprintf("%.6g", expr.Eval(test.env)) if got


!= test.want {
t.Errorf("%s: %v => %s, want %s", test.input,
test.env, got, test.want)
}
}
}

Per prima cosa, verifichiamo che il test sia superato:


$ go test -v -run=Coverage gopl.io/ch7/eval
=== Eseguire la copertura del test
--- PASS: TestCoverage (0,00s) PASS
ok gopl.io/ch7/eval 0.011s

Questo comando visualizza il messaggio di utilizzo dello strumento di copertura:


Copriutensili $ go
Utilizzo della "copertura per utensili da lavoro":
Dato un profilo di copertura prodotto da 'go test': go test -
coverprofile=c.out

Aprire un browser web che visualizzi il codice sorgente


annotato: go tool cover -html=c.out
...

Il comando go tool esegue uno degli eseguibili della toolchain di Go. Questi programmi si trovano
nella directory $GOROOT/pkg/tool/${GOOS}_${GOARCH}. Grazie a go build, raramente abbiamo bisogno
di invocarli direttamente.
Ora eseguiamo il test con il flag -coverprofile:
$ go test -run=Coverage -coverprofile=c.out gopl.io/ch7/eval
ok gopl.io/ch7/eval Copertura di 0,032s: 68,5% delle dichiarazioni

Questo flag consente di raccogliere dati di copertura strumentando il codice di produzione. In altre
parole, modifica una copia del codice sorgente in modo che prima dell'esecuzione di ogni blocco di
istruzioni venga impostata una variabile booleana, con una variabile per blocco. Poco prima dell'uscita
del programma modificato, scrive il valore di ogni variabile nel file di log specificato c.out e stampa un
riepilogo della frazione di istruzioni e s e g u i t e . (Se si desidera solo il riepilogo, utilizzare go test -
cover).

Se go test viene eseguito con il flag -covermode=count, la strumentazione per ogni blocco incrementa un
contatore invece di impostare un booleano. Il registro risultante dei conteggi di esecuzione di ciascun
blocco consente di confrontare quantitativamente i blocchi più "caldi", che vengono eseguiti più
frequentemente, con quelli più "freddi".
Dopo aver raccolto i dati, si esegue lo strumento di copertura, che elabora il log, genera un report HTML
e lo apre in una nuova finestra del browser (Figura 11.3).
$ go tool cover -html=c.out

www.it-ebooks.info
320 CAPITOLO 11. TEST

Figura 11.3. Un rapporto di copertura.

Ogni affermazione è colorata di verde se è stata trattata o di rosso se non è stata trattata. Per chiarezza,
abbiamo ombreggiato lo sfondo del testo rosso. Possiamo vedere immediatamente che nessuno dei
nostri input ha esercitato il metodo Eval dell'operatore unario. Se aggiungiamo questo nuovo caso di test
alla tabella e rieseguiamo i due comandi precedenti, il codice dell'espressione unaria diventa verde:
{"-x * -x", eval.Env{"x": 2}, "4"}

Tuttavia, le due dichiarazioni di panico rimangono rosse. Ciò non deve sorprendere, perché si
suppone che queste dichiarazioni siano irraggiungibili.
Raggiungere il 100% di copertura degli enunciati sembra un obiettivo nobile, ma di solito non è
fattibile nella pratica, né è probabile che sia un buon uso degli sforzi. Il fatto che un'istruzione venga
eseguita non significa che sia priva di bug; le istruzioni contenenti espressioni complesse devono
essere eseguite molte volte con input diversi per coprire i casi interessanti. Alcune affermazioni,
come quelle di panico di cui sopra, non possono mai essere raggiunte. Altre, come quelle che gestiscono
errori esoterici, sono difficili da esercitare ma raramente vengono raggiunte nella pratica. I test sono
fondamentalmente uno sforzo pragmatico, un compromesso tra il costo della scrittura dei test e il
costo dei fallimenti che avrebbero potuto essere evitati dai test. Gli strumenti di copertura possono
aiutare a identificare i punti più deboli, ma l'ideazione di buoni casi di test richiede lo stesso pensiero
rigoroso della programmazione in generale.

www.it-ebooks.info
SEZIONE 11.4. FUNZIONI DI BENCHMARK 321

11.4. Benchmark Funzioni

Il benchmarking è la pratica di misurare le prestazioni di un programma su un carico di lavoro fisso. In


Go, una funzione di benchmark assomiglia a una funzione di test, ma con il prefisso Benchmark e un
parametro *testing.B che fornisce la maggior parte degli stessi metodi di *testing.T, più alcuni extra
relativi alla misurazione delle prestazioni. Espone anche un campo intero N, che specifica il numero di
volte in cui eseguire l'operazione da misurare.

Ecco un benchmark per IsPalindrome che lo richiama N volte in un ciclo.


importare "testing"

func BenchmarkIsPalindrome(b *testing.B) { for i :=


0; i < b.N; i++ {
IsPalindrome("Un uomo, un piano, un canale: Panama")
}
}

Lo eseguiamo con il comando seguente. A differenza dei test, per impostazione predefinita non
viene eseguito alcun benchmark. L'argomento del flag -bench seleziona quali benchmark eseguire. È
un'espressione regolare che corrisponde ai nomi delle funzioni di benchmark, con un valore predefinito
che non corrisponde a nessuna di esse. Lo schema ''.'' fa in modo che vengano eseguiti tutti i
benchmark del pacchetto di parole, ma poiché ce n'è solo uno, -bench=IsPalindrome sarebbe stato
equivalente.
$ cd $GOPATH/src/gopl.io/ch11/word2
$ go test -bench=. PASS
BenchmarkIsPalindrome-8 1000000 1035 ns/op
ok gopl.io/ch11/word2 2.179s

Il suffisso numerico del nome del benchmark, qui 8, indica il valore di GOMAXPROCS, importante per i
benchmark concorrenti.

Il report ci dice che ogni chiamata a IsPalindrome ha richiesto circa 1,035 microsecondi, in media su
1.000.000 di esecuzioni. Poiché inizialmente il runner del benchmark non ha idea della durata
dell'operazione, effettua alcune misurazioni iniziali utilizzando piccoli valori di N e poi estrapola un
valore sufficientemente grande da consentire una misurazione stabile dei tempi.

Il motivo per cui il ciclo è implementato dalla funzione di benchmark, e non dal codice chiamante nel
driver di test, è che la funzione di benchmark ha la possibilità di eseguire qualsiasi codice di
impostazione necessario una tantum al di fuori del ciclo senza che questo si aggiunga al tempo misurato
di ogni iterazione. Se questo codice di impostazione continua a perturbare i risultati, il parametro
testing.B fornisce metodi per fermare, riprendere e resettare il timer, ma sono raramente necessari.

Ora che abbiamo un benchmark e dei test, è facile provare delle idee per rendere il programma più
veloce. Forse l'ottimizzazione più ovvia è quella di fare in modo che il secondo ciclo di IsPalindrome
interrompa il controllo a metà percorso, per evitare di fare ogni confronto due volte:

www.it-ebooks.info
322 CAPITOLO 11. TEST

n := len(lettere)/2
per i := 0; i < n; i++ {
if lettere[i] != lettere[len(lettere)-1-i] { return
false
}
}
restituire vero

Ma come spesso accade, un'ottimizzazione ovvia non sempre produce i benefici attesi. In un
esperimento, questa ha prodotto un miglioramento di appena il 4%.
$ go test -bench=. PASS
BenchmarkIsPalindrome-8 1000000 992 ns/op
ok gopl.io/ch11/word2 2.093s

Un'altra idea è quella di preallocare un array sufficientemente grande da utilizzare per le lettere,
piuttosto che espanderlo con successive chiamate ad append. Dichiarando le lettere come un array
della giusta dimensione, come questo,
lettere := make([]rune, 0, len(s)) for _, r
:= range s {
if unicode.IsLetter(r) {
lettere = append(lettere, unicode.ToLower(r))
}
}

Il miglioramento è di quasi il 35% e il runner di benchmark riporta ora la media su 2.000.000 di


iterazioni.
$ go test -bench=. PASS
BenchmarkIsPalindrome-8 2000000 697 ns/op
ok gopl.io/ch11/word2 1.468s

Come mostra questo esempio, il programma più veloce è spesso quello che effettua il minor numero di
allocazioni di memoria. Il flag della riga di comando -benchmem include le statistiche di allocazione della
memoria nel suo rapporto. Qui si confronta il numero di allocazioni prima dell'ottimizzazione:
$ go test -bench=. -benchmem PASS
BenchmarkIsPalindrome 1000000 1026 ns/op 304 B/op 4 allocazioni/op

e dopo di esso:
$ go test -bench=. -benchmem PASS
BenchmarkIsPalindrome 2000000 807 ns/op 128 B/op 1 allocazione/op

Il consolidamento delle allocazioni in un'unica chiamata a make ha eliminato il 75% delle allocazioni e
dimezzato la quantità di memoria allocata.
Benchmark come questo ci dicono il tempo assoluto richiesto per una determinata operazione, ma in
molti set- tori le domande interessanti sulle prestazioni riguardano i tempi relativi di due diverse
operazioni.

www.it-ebooks.info
SEZIONE 11.5. PROFILATURA 323

operazioni. Ad esempio, se una funzione impiega 1 ms per elaborare 1.000 elementi, quanto tempo
impiegherà per elaborarne 10.000 o un milione? Questi confronti rivelano la crescita asintotica del
tempo di esecuzione della funzione. Un altro esempio: qual è la dimensione migliore per un buffer di
I/O? I benchmark del throughput dell'applicazione su una serie di dimensioni possono aiutarci a
scegliere il buffer più piccolo che offre prestazioni soddisfacenti. Un terzo esempio: qual è l'algoritmo
che funziona meglio per un determinato lavoro? I benchmark che valutano due diversi algoritmi sugli
stessi dati di input possono spesso mostrare i punti di forza e di debolezza di ciascuno su carichi di lavoro
importanti o rappresentativi.
I benchmark comparativi sono solo codice normale. In genere hanno la forma di una singola funzione
parametrizzata, richiamata da diverse funzioni di benchmark con valori diversi, come questa:
func benchmark(b *testing.B, size int) { /* ... */ } func
Benchmark10(b *testing.B) { benchmark(b, 10) } func
Benchmark100(b *testing.B) { benchmark(b, 100) } func
Benchmark1000(b *testing.B) { benchmark(b, 1000) }

Il parametro size, che specifica la dimensione dell'input, varia da un benchmark all'altro ma è costante
all'interno di ciascun benchmark. Resistete alla tentazione di usare il parametro b.N come dimensione
dell'input. A meno che non lo si interpreti come un conteggio di iterazioni per un input di dimensioni
fisse, i risultati del benchmark saranno privi di significato.
I modelli rivelati dai benchmark comparativi sono particolarmente utili durante la progettazione del
programma, ma non dobbiamo buttare via i benchmark quando il programma funziona. Quando il
programma si evolve, o il suo input cresce, o viene distribuito su nuovi sistemi operativi o processori con
caratteristiche diverse, possiamo riutilizzare i benchmark per rivedere le decisioni di progettazione.
Esercizio 11.6: Scrivete dei benchmark per confrontare l'implementazione di PopCount nella Sezione
2.6.2 con le soluzioni dell'Esercizio 2.4 e dell'Esercizio 2.5. A che punto l'approccio basato su tabelle
raggiunge il pareggio?
Esercizio 11.7: Scrivete dei benchmark per Add, UnionWith e altri metodi di *IntSet (§6.5) utilizzando
input pseudocasuali di grandi dimensioni. Qual è la velocità di esecuzione di questi metodi? In che
modo la scelta della dimensione delle parole influisce sulle prestazioni? Quanto è veloce IntSet rispetto a
un'implementazione di set basata sul tipo di mappa incorporato?

11.5. Profilazione

I benchmark sono utili per misurare le prestazioni di operazioni specifiche, ma quando cerchiamo di
rendere più veloce un programma lento, spesso non abbiamo idea da dove cominciare. Ogni
programmatore conosce l'aforisma di Donald Knuth sull'ottimizzazione prematura, apparso in
''Structured Programming with go to Statements'' nel 1974. Sebbene sia spesso interpretato
erroneamente per significare che le prestazioni non contano, nel suo contesto originale possiamo
discernere un significato diverso:
Non c'è dubbio che il graal dell'efficienza porti ad abusi. I programmatori sprecano enormi
quantità di tempo a pensare o a preoccuparsi della velocità di operazioni non critiche.

www.it-ebooks.info
324 CAPITOLO 11. TEST

e questi tentativi di efficienza hanno in realtà un forte impatto negativo quando si considerano il
debugging e la manutenzione. Dovremmo dimenticarci delle piccole efficienze, diciamo circa il
97% delle volte: l'ottimizzazione prematura è la radice di tutti i mali.

Tuttavia, non dovremmo perdere le nostre opportunità in quel 3% critico. Un buon


programmatore non si lascerà cullare dall'autocompiacimento di questo ragionamento, ma sarà
saggio esaminare attentamente il codice critico, ma solo dopo averlo identificato. È spesso un
errore dare giudizi a priori su quali parti di un programma siano realmente critiche, poiché
l'esperienza universale dei programmatori che hanno utilizzato strumenti di misurazione è stata
che le loro ipotesi intuitive sono fallite.

Quando desideriamo esaminare attentamente la velocità dei nostri programmi, la tecnica migliore per
identificare il codice critico è il profiling. Il profiling è un approccio automatizzato alla misurazione delle
prestazioni basato sul campionamento di un certo numero di eventi di profilo durante l'esecuzione,
estrapolati poi da questi durante una fase di post-elaborazione; il riassunto statistico risultante è
chiamato profilo.

Go supporta molti tipi di profilazione, ognuno dei quali riguarda un aspetto diverso delle prestazioni,
ma tutti comportano la registrazione di una sequenza di eventi di interesse, ognuno dei quali ha una
traccia di stack di accompagnamento, ovvero lo stack delle chiamate di funzione attive al momento
dell'evento. Lo strumento go test ha un supporto integrato per diversi tipi di profiling.

Un profilo della CPU identifica le funzioni la cui esecuzione richiede la maggior parte del tempo della
CPU. Il thread attualmente in esecuzione su ogni CPU viene interrotto periodicamente dal sistema
operativo ogni pochi millisecondi; ogni interruzione registra un evento del profilo prima di riprendere la
normale esecuzione.

Un profilo heap identifica le istruzioni responsabili dell'allocazione della maggior quantità di memoria.
La libreria di profilazione campiona le chiamate alle routine interne di allocazione della memoria in
modo che, in media, venga registrato un evento di profilo ogni 512 KB di memoria allocata.

Un profilo di blocco identifica le operazioni che bloccano più a lungo le goroutine, come le chiamate di
sistema, l'invio e la ricezione di canali e l'acquisizione di blocchi. La libreria di profiling registra un
evento ogni volta che una goroutine viene bloccata da una di queste operazioni.

Per ottenere un profilo per il codice in fase di test è sufficiente attivare uno dei flag riportati di seguito.
Tuttavia, fate attenzione quando utilizzate più di un flag alla volta: i macchinari per la raccolta di un tipo
di profilo possono alterare i risultati di altri.
$ go test -cpuprofile=cpu.out
$ go test -blockprofile=block.out
$ go test -memprofile=mem.out

È facile aggiungere il supporto per il profiling anche ai programmi non di test, anche se i dettagli su
come farlo variano tra strumenti a riga di comando di breve durata e applicazioni server di lunga durata.
La profilazione è particolarmente utile nelle applicazioni di lunga durata, per cui le funzioni di
profilazione del runtime Go possono essere attivate sotto il controllo del programmatore tramite l'API
del runtime.

www.it-ebooks.info
SEZIONE 11.5. PROFILATURA 325

Una volta raccolto un profilo, è necessario analizzarlo utilizzando lo strumento pprof. È una parte
standard della distribuzione Go, ma poiché non è uno strumento di uso quotidiano, vi si accede
indirettamente usando go tool pprof. Ha decine di funzioni e opzioni, ma l'uso di base richiede solo
due argomenti, l'eseguibile che ha prodotto il profilo e il registro del profilo.
Per rendere efficiente la profilazione e risparmiare spazio, il log non include i nomi delle funzioni, ma le
funzioni sono identificate dai loro indirizzi. Questo significa che pprof ha bisogno dell'eseguibile per
dare un senso al log. Anche se go test di solito scarta l'eseguibile una volta completato il test, quando il
profiling è abilitato salva l'eseguibile come pippo.test, dove pippo è il nome del pacchetto testato.
I comandi seguenti mostrano come raccogliere e visualizzare un semplice profilo della CPU.
Abbiamo scelto uno dei benchmark del pacchetto net/http. Di solito è meglio profilare benchmark
specifici che sono stati costruiti per essere rappresentativi dei carichi di lavoro a cui si tiene. I
benchmark dei casi di test non sono quasi mai rappresentativi, per questo motivo li abbiamo
disabilitati utilizzando il filtro -run=NONE.
$ go test -run=NONE -bench=ClientServerParallelTLS64 \
-cpuprofile=cpu.log net/http PASS
BenchmarkClientServerParallelTLS64-8 1000
3141325 ns/op 143010 B/op 1747 allocs/op ok
rete/http 3.395s

$ go tool pprof -text -nodecount=10 ./http.test cpu.log 2570ms su


3590ms totali (71,59%)
Eliminati 129 nodi (cum <= 17,95ms)
Mostra i primi 10 nodi su 166 (cum >= 60ms)
piatto piatto somma% sborra cum%
%
1730 ms 48.19% 48.19% 1750 ms 48.75% crypto/elliptic.p256ReduceDegree
230 ms 6.41% 54.60% 250 ms 6.96% crypto/elliptic.p256Diff
120 ms 3.34% 57.94% 120 ms 3.34% math/big.addMulVVW
110 ms 3.06% 61.00% 110 ms 3.06% syscall.Syscall
90 ms 2.51% 63.51% 1130 ms 31.48% crypto/elliptic.p256Square
70 ms 1.95% 65.46% 120 ms 3.34% runtime.scanobject
60 ms 1.67% 67.13% 830 ms 23.12% cripto/ellittico.p256Mul
60 ms 1.67% 68.80% 190 ms 5.29% matematica/big.nat.montgomery
50 ms 1.39% 70.19% 50 ms 1.39% crypto/elliptic.p256ReduceCarry
50 ms 1.39% 71.59% 60 ms 1.67% crypto/elliptic.p256Sum

Il flag -text specifica il formato di output, in questo caso una tabella testuale con una riga per
funzione, ordinata in modo che le funzioni più ''calde'', quelle che consumano più cicli di CPU,
appaiano per prime. Il flag -nodecount=10 limita il risultato a 10 righe. Per i problemi di prestazioni
più gravi, questo formato testuale può essere sufficiente per individuare la causa.
Questo profilo ci dice che la crittografia a curva ellittica è importante per le prestazioni di questo
particolare benchmark HTTPS. Al contrario, se un profilo è dominato dalle funzioni di allocazione della
memoria del pacchetto runtime, la riduzione del consumo di memoria può essere un'ottimizzazione
utile.

www.it-ebooks.info
326 CAPITOLO 11. TEST

Per problemi più sottili, è preferibile usare uno dei visualizzatori grafici di pprof. Questi richiedono
GraphViz, che può essere scaricato da www.graphviz.org. I l flag -web visualizza un grafico diretto delle
funzioni del programma, annotato dai numeri del profilo della CPU e colorato per indicare le funzioni
più calde.
In questa sede abbiamo solo scalfito la superficie degli strumenti di profilazione di Go. Per saperne di
più, leggete l'articolo ''Profilazione dei programmi Go'' sul Go Blog.

11.6. Esempio di funzioni

Il terzo tipo di funzione trattato in modo speciale da go test è una funzione di esempio, il cui nome
inizia con Example. Non ha né parametri né risultati. Ecco una funzione di esempio per IsPalindrome:
func EsempioIsPalindrome() {
fmt.Println(IsPalindrome("Un uomo, un piano, un canale: Panama"))
fmt.Println(IsPalindrome("palindromo"))
// Uscita:
// vero
// falso
}

Le funzioni di esempio hanno tre scopi. Il principale è la documentazione: un buon esempio può essere
un modo più succinto o intuitivo di trasmettere il comportamento di una funzione di libreria rispetto
alla sua descrizione in prosa, soprattutto se usato come promemoria o riferimento rapido. Un esempio
può anche dimostrare l'interazione tra diversi tipi e funzioni appartenenti a una stessa API, mentre la
documentazione in prosa deve sempre essere collegata a un unico punto, come la dichiarazione di un
tipo o di una funzione o il pacchetto nel suo complesso. Inoltre, a differenza degli esempi all'interno dei
commenti, le funzioni di esempio sono vero codice Go, soggetto a controllo a tempo di compilazione,
quindi non diventano stantie con l'evoluzione del codice.
In base al suffisso della funzione Example, il server di documentazione web godoc associa le funzioni
di esempio alla funzione o al pacchetto che esemplificano, per cui ExampleIs- Palindrome verrebbe
mostrato con la documentazione della funzione IsPalindrome, mentre una funzione di esempio
chiamata solo Example verrebbe associata alla parola package nel suo complesso.
Il secondo scopo è che gli esempi sono test eseguibili da go test. Se la funzione di esempio contiene un
commento finale // Output: come quello sopra, il driver di test eseguirà la funzione e verificherà che
il testo stampato sul suo standard output corrisponda a quello contenuto nel commento.
Il terzo scopo di un esempio è la sperimentazione pratica. Il server godoc di golang.org utilizza il Go
Playground per consentire all'utente di modificare ed eseguire ogni funzione di esempio da un
browser web, come mostrato nella Figura 11.4. Questo è spesso il modo più veloce per farsi un'idea
di una particolare funzione o caratteristica del linguaggio. Questo è spesso il modo più veloce per
conoscere una particolare funzione o caratteristica del linguaggio.

www.it-ebooks.info
SEZIONE 11.6. FUNZIONI DI ESEMPIO 327

Figura 11.4. Un esempio interattivo di strings.Join in godoc.

Gli ultimi due capitoli del libro esaminano i pacchetti reflect e unsafe, che pochi programmatori Go
usano regolarmente e ancora meno hanno bisogno di usare. Se non avete ancora scritto alcun
programma Go sostanziale, questo è il momento giusto per farlo.

www.it-ebooks.info
Questa pagina è stata lasciata intenzionalmente in bianco

www.it-ebooks.info
12
Riflessione

Go fornisce un meccanismo per aggiornare le variabili e ispezionare i loro valori a tempo di esecuzione,
per chiamare i loro metodi e per applicare le operazioni intrinseche alla loro rappresentazione, il tutto
senza conoscere i loro tipi a tempo di compilazione. Questo meccanismo si chiama riflessione. La
riflessione ci permette anche di trattare i tipi stessi come valori di prima classe.
In questo capitolo esploreremo le funzioni di riflessione di Go per vedere come aumentano
l'espressività del linguaggio e in particolare come sono fondamentali per l'implementazione di due
importanti API: la formattazione delle stringhe fornita da fmt e la codifica dei protocolli fornita da
pacchetti come encoding/json e encoding/xml. La riflessione è anche essenziale per il meccanismo dei
template fornito dai pacchetti text/template e html/template, che abbiamo visto nella Sezione 4.6.
Tuttavia, la riflessione è complessa da ragionare e non è adatta a un uso occasionale; pertanto,
sebbene questi pacchetti siano implementati utilizzando la riflessione, non espongono la riflessione
nelle loro API.

12.1. Perché Reflection?

A volte è necessario scrivere una funzione in grado di gestire in modo uniforme valori di tipi che non
soddisfano un'interfaccia comune, che non hanno una rappresentazione nota o che non esistono nel
momento in cui progettiamo la funzione, o addirittura tutte e tre le cose.
Un esempio familiare è la logica di formattazione di fmt.Fprintf, che può stampare in modo utile un
valore arbitrario di qualsiasi tipo, anche definito dall'utente. Proviamo a implementare una funzione
simile utilizzando ciò che già conosciamo. Per semplicità, la nostra funzione accetterà un argomento e
restituirà il risultato come una stringa, come fa fmt.Sprint, quindi la chiameremo Sprint.
Si inizia con uno switch di tipo che verifica se l'argomento definisce un metodo String e, in caso
affermativo, lo chiama. Si aggiungono poi casi di switch che verificano il tipo dinamico del valore
rispetto a ciascuno dei metodi base

329

www.it-ebooks.info
330 CAPITOLO 12. RIFLESSIONE

tipi di stringa, int, bool e così via ed eseguire l'operazione di formattazione appropriata in ciascun
caso.
func Sprint(x interface{}) string { type
stringer interface {
Stringa() stringa
}
switch x := x.(tipo) { case
stringer:
return x.String() case
string:
restituire
x caso int:
restituire strconv.Itoa(x)
// ... casi simili per int16, uint32 e così via... case bool:
se x {
restituire "vero"
}
restituire "false"
predefinito:
// array, chan, func, map, pointer, slice, struct return "???"
}
}

Ma come ci si comporta con altri tipi, come []float64, map[string][]string e così via? Potremmo
aggiungere altri casi, ma il numero di questi tipi è infinito. E che dire dei tipi denominati, come
url.Values? Anche se il selettore di tipi avesse un caso per il tipo sottostante map[string][]string, non
corrisponderebbe a url.Values perché i due tipi non sono identici e il selettore di tipi non può includere
un caso per ogni tipo come url.Values perché ciò richiederebbe che questa libreria dipenda dai suoi
client.

Senza un modo per ispezionare la rappresentazione di valori di tipo sconosciuto, ci si blocca


rapidamente. Ciò di cui abbiamo bisogno è la riflessione.

12.2. reflect.Type e reflect.Value

La riflessione è fornita dal pacchetto reflect. Definisce due tipi importanti, Type e Value. Un Tipo
rappresenta un tipo Go. È un'interfaccia con molti metodi per discriminare tra i tipi e ispezionare i loro
componenti, come i campi di una struttura o i parametri di una funzione. L'unica implementazione di
reflect.Type è il descrittore di tipo (§7.5), la stessa entità che identifica il tipo dinamico di un valore
dell'interfaccia.

La funzione reflect.TypeOf accetta qualsiasi interfaccia{} e restituisce il suo tipo dinamico come un
oggetto
riflettere.Tipo:

www.it-ebooks.info
SEZIONE 12.2. REFLECT.TYPE E REFLECT.VALUE 331

t := reflect.TypeOf(3) // un reflect.Type
fmt.Println(t.String()) // "int" fmt.Println(t)
// "int"

La chiamata TypeOf(3) di cui sopra assegna il valore 3 al parametro interface{}. Ricordiamo


dalla Sezione 7.5 che un'assegnazione da un valore concreto a un tipo di interfaccia esegue una
conversione implicita di interfaccia, che crea un valore di interfaccia costituito da due
componenti: il suo tipo dinamico è il tipo dell'operando (int) e il suo valore dinamico è il valore
dell'operando (3).
Poiché reflect.TypeOf restituisce il tipo dinamico di un valore di interfaccia, restituisce sempre un
tipo con- creto. Quindi, ad esempio, il codice sottostante stampa "*os.File" e non "io.Writer". Più
avanti vedremo che reflect.Type è in grado di rappresentare anche i tipi di interfaccia.
var w io.Writer = os.Stdout
fmt.Println(reflect.TypeOf(w)) // " *os.File"

Si noti che reflect.Type soddisfa fmt.Stringer. Poiché la stampa del tipo dinamico di un valore di
interfaccia è utile per il debug e la registrazione, fmt.Printf fornisce una stenografia, %T, che utilizza
internamente reflect.TypeOf:
fmt.Printf("%T\n", 3) // "int"

L'altro tipo importante del pacchetto reflect è Value. Un reflect.Value può contenere un valore
di qualsiasi tipo. La funzione reflect.ValueOf accetta qualsiasi interfaccia{} e restituisce un
reflect.Value contenente il valore dinamico dell'interfaccia. Come per reflect.TypeOf, i risultati
di reflect.ValueOf sono sempre concreti, ma reflect.Value può contenere anche valori di
interfaccia.
v := reflect.ValueOf(3) // un reflect.Value
fmt.Println(v) // "3" fmt.Printf("%v\n",
v) // "3"
fmt.Println(v.String()) // NOTA: "<int Value>"

Come reflect.Type, anche reflect.Value soddisfa fmt.Stringer, ma a meno che Value non contenga
una stringa, il risultato del metodo String rivela solo il tipo. Si può invece utilizzare il metodo
verbo %v, che tratta in modo speciale reflect.Values.
La chiamata del metodo Type su un valore restituisce il suo tipo come reflect.Type:
t := v.Type() // un reflect.Type
fmt.Println(t.String()) // "int"

L'operazione inversa a reflect.ValueOf è il metodo reflect.Value.Interface. Esso restituisce


un'interfaccia{} che contiene lo stesso valore concreto di reflect.Value:
v := reflect.ValueOf(3) // un reflect.Value x :=
v.Interface() // un'interfaccia{} i
:= x.(int) // un int
fmt.Printf("%d\n", i) // "3"

Un reflect.Value e un'interfaccia{} possono entrambi contenere valori arbitrari. La differenza è che


un'interfaccia vuota nasconde la rappresentazione e le operazioni intrinseche del valore che contiene e
non espone nessuno dei suoi metodi, quindi, a meno che non si conosca il suo tipo dinamico e non si
utilizzi un'asserzione di tipo per

www.it-ebooks.info
332 CAPITOLO 12. RIFLESSIONE

Se si scruta al suo interno (come abbiamo fatto sopra), si può fare ben poco per il valore al suo interno.
Al contrario, un valore ha molti metodi per ispezionare il suo contenuto, indipendentemente dal suo
tipo. Usiamoli per il nostro secondo tentativo di una funzione di formattazione generale, che
chiameremo format.Any.
Invece di un interruttore di tipo, usiamo il metodo Kind di reflect.Value per discriminare i casi.
Sebbene esistano infiniti tipi, esiste solo un numero finito di tipi: i tipi di base Bool, String e tutti i
numeri; i tipi aggregati Array e Struct; i tipi di riferimento Chan, Func, Ptr, Slice e Map; i tipi di
interfaccia; e infine Invalid, che significa nessun valore. (Il valore zero di un reflect.Value ha il tipo
Invalid).
gopl.io/ch12/formato
formato del pacchetto
importare (
"reflect"
"strconv"
)
// Any formatta qualsiasi valore come stringa.
func Any(value interface{}) string {
restituire formatAtom(reflect.ValueOf(value))
}
// formatAtom formatta un valore senza ispezionare la sua struttura interna. func
formatAtom(v reflect.Value) string {
switch v.Kind() { case
reflect.Invalid:
restituire "non valido"
caso reflect.Int, reflect.Int8, reflect.Int16,
reflect.Int32, reflect.Int64:
return strconv.FormatInt(v.Int(), 10)
caso reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
reflect.Uint64, reflect.Uintptr: return
strconv.FormatUint(v.Uint(), 10)
// ... i casi in virgola mobile e complessi sono stati omessi per brevità...
case reflect.Bool:
return strconv.FormatBool(v.Bool()) case
reflect.String:
restituire strconv.Quote(v.String())
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Slice, reflect.Map: return
v.Type().String() + " 0x" +
strconv.FormatUint(uint64(v.Pointer()), 16)
default: // reflect.Array, reflect.Struct, reflect.Interface return
v.Type().String() + " value"
}
}

Finora, la nostra funzione tratta ogni valore come un oggetto indivisibile senza struttura interna, quindi
formatAtom. Per i tipi aggregati (struct e array) e le interfacce stampa solo il tipo del valore, mentre per i
tipi di riferimento (canali, funzioni, puntatori, slices e mappe) stampa il tipo e l'indirizzo di riferimento
in esadecimale. Si tratta di una soluzione non ideale, ma comunque importante

www.it-ebooks.info
SEZIONE 12.3. VISUALIZZAZIONE, UNA STAMPANTE DI VALORI 333
RICORSIVA

e poiché Kind si occupa solo della rappresentazione sottostante, for- mat.Any funziona anche per i
tipi denominati. Ad esempio:
var x int64 = 1
var d time.Duration = 1 * time.Nanosecond
fmt.Println(format.Any(x)) // "1"
fmt.Println(format.Any(d)) // "1"
fmt.Println(format.Any([]int64{x})) // "[]int64 0x8202b87b0"
fmt.Println(format.Any([]time.Duration{d})) // "[]time.Duration 0x8202b87e0"

12.3. Display, un valore ricorsivo Stampante

Successivamente vedremo come migliorare la visualizzazione dei tipi compositi. Piuttosto che cercare di
copiare esattamente fmt.Sprint, costruiremo una funzione di utilità per il debug chiamata Display che,
dato un valore x arbitrariamente complesso, stampa la struttura completa di quel valore, etichettando
ogni elemento con il percorso attraverso il quale è stato trovato. Cominciamo con un esempio.
e, _ := eval.Parse("sqrt(A / pi)") Display("e",
e)

Nella chiamata qui sopra, l'argomento di Display è un albero di sintassi ricavato dal valutatore di
espressioni della Sezione 7.9. L'output di Display è mostrato di seguito:
Visualizzare e (eval.call):
e.fn = "sqrt"
e.args[0].type = eval.binary e.args[0].value.op =
47 e.args[0].value.x.type = eval.Var
e.args[0].value.x.value = "A"
e.args[0].value.y.type = eval.Var
e.args[0].value.y.value = "pi"

Se possibile, si dovrebbe evitare di esporre la riflessione nell'API di un pacchetto. Definiamo una


funzione display non esportata per svolgere il vero lavoro di ricorsione ed esportiamo Display, un
semplice wrapper che accetta un parametro interface{}:
gopl.io/ch12/display
func Display(name string, x interface{}) {
fmt.Printf("Display %s (%T):\n", name, x) display(name,
reflect.ValueOf(x))
}

Nella visualizzazione, useremo la funzione formatAtom definita in precedenza per stampare valori
elementari - tipi di base, funzioni e canali - ma useremo i metodi di reflect.Value per visualizzare
ricorsivamente ogni componente di un tipo più complesso. Man mano che la ricorsione scende, la
stringa del percorso, che inizialmente descrive il valore di partenza (per esempio, "e"), verrà aumentata
per indicare come si è arrivati al valore attuale (per esempio, "e.args[0].value").

www.it-ebooks.info
334 CAPITOLO 12. RIFLESSIONE

Poiché non pretendiamo più di implementare fmt.Sprint, useremo il pacchetto fmt per mantenere il
nostro esempio breve.
func display(path string, v reflect.Value) { switch
v.Kind() {
caso reflect.Invalid:
fmt.Printf("%s = invalido\n", percorso)
case reflect.Slice, reflect.Array:
for i := 0; i < v.Len(); i++ { display(fmt.Sprintf("%s[%d]",
path, i)), v.Index(i))
}
case reflect.Struct:
per i := 0; i < v.NumField(); i++ {
fieldPath := fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name)
display(fieldPath, v.Field(i))
}
caso reflect.Map:
for _, key := range v.MapKeys() {
display(fmt.Sprintf("%s[%s]", path,
formatAtom(chiave)), v.MapIndex(chiave))
}
case reflect.Ptr: if
v.IsNil() {
fmt.Printf("%s = nil\n", percorso)
} else {
display(fmt.Sprintf("(*%s)", percorso), v.Elem())
}
case reflect.Interface: if
v.IsNil() {
fmt.Printf("%s = nil\n", percorso)
} else {
fmt.Printf("%s.type = %s\n", path, v.Elem().Type()) display(path+".value",
v.Elem())
}
default: // tipi di base, canali, funzioni fmt.Printf("%s =
%s\n", path, formatAtom(v))
}
}

Discutiamo i casi in ordine sparso.


Fette e array: La logica è la stessa per entrambi. Il metodo Len restituisce il numero di elementi di una
slice o di un valore di un array, mentre Index(i) recupera l'elemento all'indice i, sempre come
reflect.Value; va in panico se i è fuori dai limiti. Queste sono analoghe alle operazioni integrate
len(a) e a[i] sulle sequenze. La funzione display richiama ricorsivamente se stessa su ogni
elemento della sequenza, aggiungendo la notazione di pedice "[i]" al percorso.
Sebbene reflect.Value disponga di molti metodi, solo alcuni sono sicuri da richiamare per
qualsiasi valore. Ad esempio, il metodo Index può essere chiamato su valori del tipo Slice, Array o
String, ma va in panico per qualsiasi altro tipo.

www.it-ebooks.info
SEZIONE 12.3. VISUALIZZAZIONE, UNA STAMPANTE DI VALORI 335
RICORSIVA

Strutture: Il metodo NumField riporta il numero di campi della struttura e Field(i) restituisce il
valore dell'i-esimo campo come reflect.Value. L'elenco dei campi include quelli promossi da campi
anonimi. Per aggiungere la notazione del selettore di campo ".f" al percorso, dobbiamo ottenere il
reflect.Type della struct e accedere al nome del suo i-esimo campo.

Mappe: Il metodo MapKeys restituisce una fetta di reflect.Values, una per ogni chiave della mappa.
Come di consueto quando si itera su una mappa, l'ordine è indefinito. MapIndex(chiave) restituisce il
valore corrispondente alla chiave. Aggiungiamo al percorso il pedice "[chiave]". (Stiamo tagliando un
angolo. Il tipo di chiave di una mappa non è limitato ai tipi che formatAtom gestisce meglio; anche
array, strutture e interfacce possono essere chiavi di mappa valide. L'estensione di questo caso per
stampare la chiave per intero è l'esercizio 12.1).

Puntatori: Il metodo Elem restituisce la variabile puntata da un puntatore, sempre come


reflect.Value. Questa operazione sarebbe sicura anche se il valore del puntatore fosse nullo, nel
qual caso il risultato sarebbe di tipo Invalid, ma utilizziamo IsNil per rilevare esplicitamente i
puntatori nulli in modo da poter stampare un messaggio più appropriato. Il percorso è preceduto
da un "*" e da parentesi per evitare ambiguità.

Interfacce: Anche in questo caso, usiamo IsNil per verificare se l'interfaccia è nil e, in caso contrario,
recuperiamo il suo valore dinamico usando v.Elem() e stampiamo il suo tipo e valore.

Ora che la nostra funzione Display è completa, mettiamola al lavoro. Il tipo di filmato che segue è una leggera
variazione di quello descritto nella Sezione 4.5:
tipo Film struct { Titolo,
Sottotitolo stringa Anno
int
Colore bool
Attore map[stringa]stringa
Oscar []stringa
Sequel *corda
}

Dichiariamo un valore di questo tipo e vediamo cosa fa Display con esso:


strangelove := Movie{
Titolo: "Il dottor Stranamore",
Sottotitolo: "Come ho imparato a smettere di preoccuparmi e ad amare la
bomba", Anno: 1964,
Colore: falso,
Attore: map[string]string{
"Il dottor Stranamore": "Peter Sellers",
"Grp. Cap. Lionel Mandrake": "Peter Sellers",
"Pres. Merkin Muffley": "Peter Sellers",
"Gen. Buck Turgidson": "George C. Scott",
"Brig. Gen. Jack D. Ripper": "Sterling Hayden",
Maj. T.J. "King" Kong": "Slim Pickens",
},

www.it-ebooks.info
336 CAPITOLO 12. RIFLESSIONE

Oscar: []stringa{
"Miglior attore (Nomin.)",
"Miglior sceneggiatura non originale
(nomin.)", "Miglior regia (nomin.)",
"Miglior film (Nomin.)",
},
}

Viene stampata la chiamata Display("strangelove", strangelove):


Visualizzare Stranamore (display.Movie):
strangelove.Title = "Dr. Strangelove"
strangelove.Subtitle = "Come ho imparato a smettere di preoccuparmi e ad amare la bomba"
strangelove.Year = 1964
strangelove.Color = false
strangelove.Actor["Gen. Buck Turgidson"] = "George C. Scott"
strangelove.Actor["Brig. Gen. Jack D. Ripper"] = "Sterling Hayden"
strangelove.Actor["Maj. T.J. \ "King\" Kong"] = "Slim Pickens"
strangelove.Actor["Dr. Strangelove"] = "Peter Sellers" strangelove.Actor["Grp.
Capt. Lionel Mandrake"] = "Peter Sellers" strangelove.Actor["Pres. Merkin
Muffley"] = "Peter Sellers" strangelove.Oscars[0] = "Miglior Attore (Nomin.)"
strangelove.Oscars[1] = "Miglior Sceneggiatura non originale (Nomin.)"
strangelove.Oscars[2] = "Miglior Regia (Nomin.)" strangelove.Oscars[3] =
"Miglior Film (Nomin.)"
strangelove.Sequel = nil

Si può usare Display per visualizzare gli interni dei tipi di libreria, come *os.File:
Visualizza("os.Stderr", os.Stderr)
// Uscita:
// Visualizza os.Stderr (*os.File):
// ( *( *os.Stderr).file).fd = 2
// (*(*os.Stderr).file).nome = "/dev/stderr"
// ( *(*os.Stderr).file).nepipe = 0

Si noti che anche i campi non esportati sono visibili alla riflessione. Si noti che l'output particolare di
questo esempio può variare da una piattaforma all'altra e può cambiare nel tempo con l'evoluzione delle
librerie. (Possiamo anche applicare Display a un reflect.Value e osservarlo mentre attraversa la
rappresentazione interna del descrittore di tipo per *os.File. L'output della chiamata Display("rV",
reflect.ValueOf(os.Stderr)) è mostrato di seguito, anche se n a t u r a l m e n t e il vostro
chilometraggio può variare:
Visualizzazione di rV (reflect.Value):
(*rV.typ).size = 8
(*rV.typ).hash = 871609668
(*rV.typ).align = 8
(*rV.typ).fieldAlign = 8
(*rV.typ).kind = 22
(*(*rV.typ).string) = "*os.File"

www.it-ebooks.info
SEZIONE 12.3. VISUALIZZAZIONE, UNA STAMPANTE DI VALORI 337
RICORSIVA

(*(*(*(*rV.typ).uncommonType).methods[0].name) = "Chdir"
(*(*(*(*(*rV.typ).uncommonType).methods[0].mtyp).string) = "func() errore"
(*(*(*(*(*rV.typ).uncommonType).methods[0].typ).string) = "func(*os.File) errore"
...

Osservate la differenza tra questi due esempi:


var i interface{} = 3 Display("i",

i)
// Uscita:
// Visualizzare i (int):
// i = 3

Visualizza("&i", &i)
// Uscita:
// Visualizzare &i (*interfaccia {}):
// (*&i).type = int
// (*&i).valore = 3

Nel primo esempio, Display chiama reflect.ValueOf(i), che restituisce un valore di tipo Int.
Come abbiamo detto nella sezione 12.2, reflect.ValueOf restituisce sempre un valore di tipo concreto,
poiché estrae il contenuto di un valore di interfaccia.
Nel secondo esempio, Display chiama reflect.ValueOf(&i), che restituisce un puntatore a i, di
tipo Ptr. Il caso switch per Ptr chiama Elem su questo valore, che restituisce un Valore che rappresenta la
variabile i stessa, di tipo Interfaccia. Un valore ottenuto indirettamente, come questo, può
rappresentare qualsiasi valore, comprese le interfacce. La funzione di visualizzazione richiama se stessa in
modo ricorsivo e questa volta stampa componenti separati per il tipo dinamico e il valore dell'interfaccia.
Nella versione attuale, Display non termina mai se incontra un ciclo nel grafo degli oggetti, come questa
lista collegata che si mangia la coda:
// una struttura che punta a se stessa
type Ciclo struct{ Valore int; Coda *Ciclo } var c
Ciclo
c = Ciclo{42, &c}
Visualizza("c", c)

Il display stampa questa espansione in continua crescita:


Display c (display.Cycle):
c.Value = 42
(*c.Tail).Value = 42
(*(*c.Tail).Tail).Value = 42
(*(*c.coda).coda).coda).valore = 42
...all'infinito...

Molti programmi Go contengono almeno alcuni dati ciclici. Rendere Display robusto contro tali cicli è
complicato e richiede una contabilità aggiuntiva per registrare l'insieme di riferimenti che sono stati
seguiti fino a quel momento; è anche costoso. Una soluzione generale richiede caratteristiche del
linguaggio non sicure, come vedremo in Sezione 13.3.

www.it-ebooks.info
338 CAPITOLO 12. RIFLESSIONE

I cicli rappresentano un problema minore per fmt.Sprint perché raramente cerca di stampare la
struttura completa. Per esempio, quando incontra un puntatore, interrompe la ricorsione stampando il
valore numerico del puntatore. Può rimanere bloccata nel tentativo di stampare una slice o una mappa
che contiene se stessa come elemento, ma questi rari casi non giustificano il considerevole problema
aggiuntivo della gestione dei cicli.
Esercizio 12.1: Estendere Display in modo che possa visualizzare mappe le cui chiavi sono strutture o array.
Esercizio 12.2: Rendere sicuro l'uso della visualizzazione su strutture di dati cicliche, limitando il
numero di passi necessari prima di abbandonare la ricorsione. (Nella Sezione 13.3 vedremo un altro
modo per individuare i cicli).

12.4. Esempio: Codifica delle espressioni S-

Display è una routine di debug per la visualizzazione di dati strutturati, ma non è lontanamente in grado
di codificare o marshallare oggetti Go arbitrari come messaggi in una notazione portatile adatta alla
comunicazione tra processi.
Come abbiamo visto nella Sezione 4.5, la libreria standard di Go supporta una varietà di formati, tra cui
JSON, XML e ASN.1. Un'altra notazione ancora molto utilizzata è quella delle espressioni S, la sintassi del
Lisp. A differenza delle altre notazioni, le espressioni S non sono supportate dalla libreria standard di
Go, anche perché non hanno una definizione universalmente accettata, nonostante i numerosi tentativi
di standardizzazione e l'esistenza di molte implementazioni.
In questa sezione verrà definito un pacchetto che codifica oggetti Go arbitrari utilizzando una notazione
S-expression che supporta i seguenti costrutti:
42 intero
"ciao" stringa (con citazione in stile Go)
pippo simbolo (un nome non quotato)
(1 2 3) elenco (zero o più elementi racchiusi tra parentesi)

I booleani sono tradizionalmente codificati usando il simbolo t per true e la lista vuota () o il
simbolo nil per false, ma per semplicità la nostra implementazione li ignora. Ignora anche i canali e
le funzioni, poiché il loro stato è opaco alla riflessione. Ignora anche i numeri reali e complessi in virgola
mobile e le interfacce. L'aggiunta del supporto per questi ultimi è l'esercizio 12.3.
Codificheremo i tipi di Go usando le espressioni S come segue. Gli interi e le stringhe sono codificati nel
modo più ovvio. I valori nulli sono codificati come il simbolo nil. Array e slices sono codificati con la
notazione delle liste.
Le strutture sono codificate come un elenco di legami di campo; ogni legame di campo è un elenco a
due elementi, il cui primo elemento (un simbolo) è il nome del campo e il secondo elemento è il valore
del campo. Anche le mappe sono codificate come un elenco di coppie, dove ogni coppia rappresenta la
chiave e il valore di una voce della mappa. Tradizionalmente, le espressioni S rappresentano elenchi di
coppie chiave/valore utilizzando una singola cella cons (chiave . valore) per ogni coppia, anziché un
elenco a due elementi, ma per semplificare la decodifica ignoreremo la notazione di elenco punteggiato.

www.it-ebooks.info
SEZIONE 12.4. ESEMPIO: CODIFICA DELLE ESPRESSIONI S 339

La codifica viene effettuata da un'unica funzione ricorsiva, Encode, mostrata di seguito. La sua struttura
è essenzialmente la stessa di Display nella sezione precedente:

gopl.io/ch12/sexpr
func encode(buf *bytes.Buffer, v reflect.Value) error { switch
v.Kind() {
case reflect.Invalid: buf.WriteString("nil")

case reflect.Int, reflect.Int8, reflect.Int16,


reflect.Int32, reflect.Int64: fmt.Fprintf(buf,
"%d", v.Int())

case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,


reflect.Uint64, reflect.Uintptr: fmt.Fprintf(buf, "%d",
v.Uint())

case reflect.String:
fmt.Fprintf(buf, "%q", v.String())

caso reflect.Ptr:
return encode(buf, v.Elem())

caso reflect.Array, reflect.Slice: // (valore ...)


buf.WriteByte('(')
per i := 0; i < v.Len(); i++ { se
i >0{
buf.WriteByte(' ' )
}
if err := encode(buf, v.Index(i)); err := nil { return
err
}
}
buf.WriteByte(')')

case reflect.Struct: // ((nome valore) ...)


buf.WriteByte('(')
per i := 0; i < v.NumField(); i++ { se i
>0{
buf.WriteByte(' ' )
}
fmt.Fprintf(buf, "(%s ", v.Type().Field(i).Name) if err
:= encode(buf, v.Field(i)); err != nil {
restituire err
}
buf.WriteByte(')')
}
buf.WriteByte(')')

www.it-ebooks.info
340 CAPITOLO 12. RIFLESSIONE

case reflect.Map: // ((chiave valore) ...)


buf.WriteByte('(')
per i, chiave := intervallo v.MapKeys() {
se i > 0 {
buf.WriteByte(' ' )
}
buf.WriteByte('(')
if err := encode(buf, key); err : = nil { return err
}
buf.WriteByte(' ' )
if err := encode(buf, v.MapIndex(key)); err := nil { return
err
}
buf.WriteByte(')')
}
buf.WriteByte(')')

default: // float, complex, bool, chan, func, interface return


fmt.Errorf("unsupported type: %s", v.Type())
}
restituire nil
}

La funzione Marshal racchiude il codificatore in un'API simile a quella degli altri pacchetti di codifica:
// Marshal codifica un valore Go in forma di espressione S. func
Marshal(v interface{}) ([]byte, error) {
var buf bytes.Buffer
if err := encode(&buf, reflect.ValueOf(v)); err := nil { return
nil, err
}
restituire buf.Bytes(), nil
}

Ecco l'output di Marshal applicato alla variabile strangelove dalla Sezione 12.3:
((Titolo "Il dottor Stranamore") (Sottotitolo "Come imparai a smettere di preoccuparmi
e ad amare la bomba") (Anno 1964) (Attore (("Grp. Capt. Lionel Mandrake" "Peter Sell
ers") ("Pres. Merkin Muffley" "Peter Sellers") ("Gen. Buck Turgidson" "Geor ge C. Scott")
("Brig. Gen. Jack D. Ripper" "Sterling Hayden") ("Magg. T.J. Kong" "Slim Pickens") ("Dr.
Stranamore" "Peter Sellers")) (Oscar ("Miglior attore (Nomin.)" "Migliore
sceneggiatura non originale (Nomin.)" "Miglior regia (Nomin.)" "Miglior film (Nomin.)")
(Sequel nullo))

L'intero output appare su un'unica lunga riga con spazi minimi, rendendo difficile la lettura. Ecco lo
stesso output formattato manualmente secondo le convenzioni delle espressioni S. La scrittura di un
pretty-printer per le espressioni S viene lasciata come esercizio (impegnativo); il download da gopl.io
include una versione semplice.

www.it-ebooks.info
SEZIONE 12.5. IMPOSTAZIONE DELLE VARIABILI CON 341
REFLECT.VALUE

((Titolo "Il dottor Stranamore"))


(Sottotitolo "Come ho imparato a smettere di preoccuparmi e ad amare la
bomba") (Anno 1964)
(Attore (("Grp. Cap. Lionel Mandrake" "Peter Sellers") ("Pres.
Merkin Muffley" "Peter Sellers") ("Gen. Buck
Turgidson" "George C. Scott")
("Brig. Gen. Jack D. Ripper" "Sterling Hayden") ("Magg.
T.J. "King" Kong" "Slim Pickens") ("Dr. Stranamore"
"Peter Sellers"))
(Oscar ("Miglior Attore (Nomin.)"
"Migliore sceneggiatura non originale
(nomin.)". "Miglior regia (Nomin.)"
"Miglior film (Nomin.)") (Sequel
nil))

Come le funzioni fmt.Print, json.Marshal e Display, sexpr.Marshal va in loop per sempre se


chiamata con dati ciclici.
Nella Sezione 12.6 verrà illustrata l'implementazione della corrispondente funzione di decodifica delle
espressioni S, ma prima di arrivare a questo punto è necessario capire come la riflessione possa essere
utilizzata per aggiornare le variabili del programma.
Esercizio 12.3: Implementare i casi mancanti della funzione encode. Codificare i booleani come t e nil, i
numeri in virgola mobile usando la notazione di Go e i numeri complessi come 1+2i come
#C(1,0 2,0). Le interfacce possono essere codificate come una coppia di nomi di tipi e valori,
ad esempio ("[]int" (1 2 3)), ma bisogna fare attenzione all'ambiguità di questa notazione: il
metodo reflect.Type.String può restituire la stessa stringa per tipi diversi.
Esercizio 12.4: Modificare encode per stampare l'espressione S nello stile mostrato sopra.
Esercizio 12.5: Adattare encode per emettere JSON anziché espressioni S. Testate il vostro codificatore
usando il decodificatore standard, json.Unmarshal.
Esercizio 12.6: Adattare encode in modo c h e , come ottimizzazione, non codifichi un campo il cui
valore è il valore zero del suo tipo.
Esercizio 12.7: Creare un'API di streaming per il decodificatore di espressioni S, seguendo lo stile di
json.Decoder (§4.5).

12.5. Impostazione di variabili con reflect.Value

Finora la riflessione ha solo interpretato i valori del nostro programma in vari modi. Lo scopo di questa
sezione, tuttavia, è quello di cambiarli.
Ricordiamo che alcune espressioni di Go come x, x.f[1] e *p denotano variabili, ma altre come x + 1
e f(2) no. Una variabile è una locazione di memoria indirizzabile che contiene un valore e il cui
valore può essere aggiornato attraverso quell'indirizzo.

www.it-ebooks.info
342 CAPITOLO 12. RIFLESSIONE

Una distinzione simile si applica a reflect.Values. Alcuni sono indirizzabili, altri no. Si considerino le
seguenti dichiarazioni:
x := 2 // valore tipo variabile?
a := reflect.ValueOf(2) // 2 int no
b := reflect.ValueOf(x) // 2 int no
c := reflect.ValueOf(&x) // &x *int no
d := c.Elem() // 2 int sì (x)

Il valore all'interno di a non è indirizzabile. Si tratta semplicemente di una copia dell'intero 2. Lo stesso vale
per
b. Anche il valore all'interno di c non è indirizzabile, essendo una copia del valore del puntatore &x.
Infatti, nessun reflect.Value restituito da reflect.ValueOf(x) è indirizzabile. Ma d, derivato da c
tramite la dereferenziazione del puntatore al suo interno, fa riferimento a una variabile ed è quindi
indirizzabile. Possiamo utilizzare questo approccio, chiamando reflect.ValueOf(&x).Elem(), per
ottenere un Valore indirizzabile per qualsiasi variabile x.
Possiamo chiedere a un reflect.Value se è indirizzabile attraverso il suo metodo CanAddr:
fmt.Println(a.CanAddr()) // "falso"
fmt.Println(b.CanAddr()) // "falso"
fmt.Println(c.CanAddr()) // "falso"
fmt.Println(d.CanAddr()) // "vero"

Otteniamo un reflect.Value indirizzabile ogni volta che indirettamente passiamo attraverso un


puntatore, anche se siamo partiti da un Value non indirizzabile. Tutte le regole usuali per
l'indirizzabilità hanno degli analoghi per la riflessione. Ad esempio, poiché l'espressione di
indicizzazione della fetta e[i] segue implicitamente un puntatore, essa è indirizzabile anche se
l'espressione e non lo è. Per analogia, reflect.ValueOf(e).Index(i) si riferisce a una variabile ed è
quindi indirizzabile anche se reflect.ValueOf(e) non lo è.
Per recuperare la variabile da un reflect.Value indirizzabile sono necessari tre passaggi.
Innanzitutto, si chiama Addr(), che restituisce un valore contenente un puntatore alla variabile.
Successivamente, si chiama Interface() su questo valore, che restituisce un valore interface{}
contenente il puntatore. Infine, se conosciamo il tipo della variabile, possiamo usare un'asserzione di
tipo per recuperare il contenuto dell'interfaccia come un normale puntatore. Possiamo quindi
aggiornare la variabile attraverso il puntatore:
x := 2
d := reflect.ValueOf(&x).Elem() // d si riferisce alla variabile x px
:= d.Addr().Interface().(*int) // px := &x
*px = 3 // x = 3
fmt.Println(x) // "3"

In alternativa, è possibile aggiornare la variabile a cui fa riferimento un reflect.Value indirizzabile


direttamente, senza utilizzare un puntatore, chiamando il metodo reflect.Value.Set:
d.Set(reflect.ValueOf(4))
fmt.Println(x) // "4"

Gli stessi controlli di assegnabilità che vengono normalmente eseguiti dal compilatore sono eseguiti in
fase di esecuzione dai metodi Set. Qui sopra, la variabile e il valore sono entrambi di tipo int, ma se la
variabile fosse stata un int64, il programma sarebbe andato in panico, quindi è fondamentale assicurarsi
che il valore sia assegnabile al tipo della variabile:

www.it-ebooks.info
SEZIONE 12.5. IMPOSTAZIONE DELLE VARIABILI CON 343
REFLECT.VALUE

d.Set(reflect.ValueOf(int64(5))) // panico: int64 non è assegnabile a int

E naturalmente anche la chiamata a Set su un reflect.Value non indirizzabile va in panico:


x := 2
b := reflect.ValueOf(x)
b.Set(reflect.ValueOf(3)) // panico: impostazione con valore non indirizzabile

Esistono varianti di Set specializzate per alcuni gruppi di tipi di base: SetInt, SetUint, Set- String,
SetFloat e così via:
d := reflect.ValueOf(&x).Elem()
d.SetInt(3)
fmt.Println(x) // "3"

Per certi versi questi metodi sono più indulgenti. SetInt, per esempio, avrà successo se il tipo della
variabile è un qualche tipo di intero firmato, o anche un tipo nominato il cui tipo sottostante è un intero
firmato, e se il valore è troppo grande sarà tranquillamente troncato per adattarsi. Ma attenzione: se si
chiama SetInt su un reflect.Value che fa riferimento a una variabile interface{}, si va in panico,
anche se Set avrebbe successo.
x := 1
rx : = reflect.ValueOf(&x).Elem() rx.SetInt(2) // OK, x
= 2 rx.Set(reflect.ValueOf(3)) // OK, x = 3
rx.SetString("ciao") // panico: la stringa non è assegnabile a int
rx.Set(reflect.ValueOf("hello")) // panico: la stringa non è assegnabile all'int

var y interfaccia{}
ry := reflect.ValueOf(&y).Elem()
ry.SetInt(2) // panico: SetInt chiamato sull'interfaccia Value
ry.Set(reflect.ValueOf(3)) // OK, y = int(3)
ry.SetString("ciao") // panico: SetString chiamato sull'interfaccia Value
ry.Set(reflect.ValueOf("hello")) // OK, y = "ciao"

Quando abbiamo applicato Display a os.Stdout, abbiamo scoperto che reflection può leggere i
valori dei campi non esportati delle struct che sono inaccessibili secondo le regole usuali del linguaggio,
come il campo fd int di una struct os.File su una piattaforma Unix-like. Tuttavia, reflection non
può aggiornare tali valori:
stdout := reflect.ValueOf(os.Stdout).Elem() // *os.Stdout, un os.File var
fmt.Println(stdout.Type()) // "os.File"
fd := stdout.FieldByName("fd")
fmt.Println(fd.Int()) // "1"
fd.SetInt(2) // panico: campo non esportato

Un reflect.Value indirizzabile registra se è stato ottenuto attraversando un campo struct non esportato
e, in tal caso, ne impedisce la modifica. Di conseguenza, CanAddr non è di solito il controllo giusto da
utilizzare prima di impostare una variabile. Il metodo correlato CanSet indica se un reflect.Value è
indirizzabile e impostabile:
fmt.Println(fd.CanAddr(), fd.CanSet()) // "vero falso"

www.it-ebooks.info
344 CAPITOLO 12. RIFLESSIONE

12.6. Esempio: Decodifica delle espressioni S-

Per ogni funzione Marshal fornita dai pacchetti encoding/... della libreria standard, esiste una
corrispondente funzione Unmarshal che esegue la decodifica. Per esempio, come abbiamo visto nella
Sezione 4.5, data una fetta di byte contenente dati codificati JSON per il nostro tipo Movie (§12.3),
possiamo decodificarla in questo modo:
dati := []byte{/* ... */} var
movie Movie
err := json.Unmarshal(data, &movie)

La funzione Unmarshal utilizza la riflessione per modificare i campi della variabile film esistente, creando
nuove mappe, strutture e slices, determinate dal tipo Movie e dal contenuto dei dati in arrivo.
Implementiamo ora una semplice funzione Unmarshal per le espressioni S, analoga alla funzione
standard json.Unmarshal usata in precedenza e l'inverso della nostra precedente sexpr.Marshal.
Dobbiamo avvertire che un'implementazione robusta e generale richiede una quantità di codice
sostanzialmente superiore a quella che può essere contenuta in questo esempio, che è già molto
lungo, per cui abbiamo preso molte scorciatoie. Supportiamo solo un sottoinsieme limitato di
espressioni S e non gestiamo completamente gli errori. Il codice ha lo scopo di illustrare la riflessione,
non il parsing.
Il lesser utilizza il tipo Scanner del pacchetto text/scanner per suddividere un flusso di input in una
sequenza di token come commenti, identificatori, letterali di stringa e letterali numerici. Il metodo
Scan dello scanner fa avanzare lo scanner e restituisce il tipo del token successivo, che ha il tipo
rune. La maggior parte dei token, come '(', consiste in una singola runa, ma il pacchetto
text/scanner rappresenta i tipi dei token a più caratteri Ident, String e Int usando piccoli valori
negativi di tipo rune. Dopo una chiamata a Scan che restituisce uno di questi tipi di token, il
metodo TokenText dello scanner restituisce il testo del token.
Poiché un parser tipico può avere bisogno di ispezionare il token corrente più volte, ma il metodo Scan fa
avanzare lo scanner, lo avvolgiamo in un tipo helper chiamato lexer, che tiene traccia dell'ultimo token
restituito da Scan.
gopl.io/ch12/sexpr
type lexer struct {
scanner.Scanner
token rune // il token corrente
}

func (lex *lexer) next() { lex.token = lex.scan.Scan() } func


(lex *lexer) text() string { return lex.scan.TokenText() }

func (lex *lexer) consume(want rune) {


if lex.token != want { // NOTA: non è un esempio di buona gestione degli errori.
panic(fmt.Sprintf("got %q, want %q", lex.text(), want))
}
lex.next()
}

www.it-ebooks.info
SEZIONE 12.6. ESEMPIO: DECODIFICA DELLE ESPRESSIONI S 345

Passiamo ora al parser. Si compone di due funzioni principali. La prima di queste, read, legge
l'espressione S che inizia con il token corrente e aggiorna la variabile a cui fa riferimento l'indirizzabile
reflect.Value v.
func read(lex *lexer, v reflect.Value) { switch
lex.token {
caso scanner.Ident:
// Gli unici identificatori validi sono
// "nil" e i nomi dei campi struct. if
lex.text() == "nil" {
v.Set(reflect.Zero(v.Type())
lex.next()
ritorno
}
case scanner.String:
s, _ := strconv.Unquote(lex.text()) // NOTA: ignorare gli errori v.SetString(s)
lex.next()
ritorno
case scanner.Int:
i, _ := strconv.Atoi(lex.text()) // NOTA: ignorare gli errori
v.SetInt(int64(i))
lex.next()
ritorno
caso '(':
lex.next() readList(lex,
v)
lex.next() // consuma ')' return
}
panic(fmt.Sprintf("token inatteso %q", lex.text())
}

Le nostre espressioni S utilizzano gli identificatori per due scopi distinti, i nomi dei campi della struct
e il valore nil per un puntatore. La funzione di lettura gestisce solo quest'ultimo caso. Quando
incontra lo scanner.Ident "nil", imposta v sul valore zero del suo tipo utilizzando la funzione
reflect.Zero. Per qualsiasi altro identificatore, segnala un errore. La funzione readList, che vedremo
tra poco, gestisce gli identificatori utilizzati come nomi di campi della struct.
Un token '(' indica l'inizio di un elenco. La seconda funzione, readList, decodifica un elenco in
una variabile di tipo composito - mappa, struct, slice o array - a seconda del tipo di variabile Go che
stiamo popolando. In ogni caso, il ciclo continua ad analizzare gli elementi finché non incontra la
parentesi di chiusura corrispondente, ')', come rilevato dalla funzione endList.
La parte interessante è la ricorsione. Il caso più semplice è quello di un array. Finché non si vede la
chiusura ')', si usa Index per ottenere la variabile per ogni elemento dell'array e si fa una chiamata
ricorsiva a read per popolarla. Come in molti altri casi di errore, se i dati in ingresso fanno sì che il
decodificatore indicizzi oltre la fine dell'array, il decodificatore va nel panico. Un approccio simile
viene utilizzato per le slice, tranne per il fatto che occorre creare una nuova variabile per ogni
elemento, popolarla e quindi aggiungerla alla slice.

www.it-ebooks.info
346 CAPITOLO 12. RIFLESSIONE

I cicli per le strutture e le mappe devono analizzare una sottolista (valore chiave) a ogni
iterazione. Per le struct, la chiave è un simbolo che identifica il campo. Analogamente al caso degli
array, si ottiene la variabile esistente per il campo della struct usando FieldByName e si effettua
una chiamata ricorsiva per popolarla. Per le mappe, la chiave può essere di qualsiasi tipo e,
analogamente al caso delle fette, si crea una nuova variabile, la si popola ricorsivamente e infine si
inserisce la nuova coppia chiave/valore nella mappa.

func readList(lex *lexer, v reflect.Value) { switch


v.Kind() {
case reflect.Array: // (item ...) for i :=
0; !endList(lex); i++ {
read(lex, v.Index(i))
}

caso reflect.Slice: // (item ...) for


!endList(lex) {
item := reflect.New(v.Type().Elem()).Elem()
read(lex, item)
v.Set(reflect.Append(v, item))
}

case reflect.Struct: // ((nome valore) ...) for


!endList(lex) {
lex.consume('(')
se lex.token != scanner.Ident {
panic(fmt.Sprintf("ottenuto token %q, vuole nome campo", lex.text())
}
nome := lex.text()
lex.next()
read(lex, v.FieldByName(name))
lex.consume(')')
}

caso reflect.Map: // ((valore chiave) ...)


v.Set(reflect.MakeMap(v.Type()) for
!endList(lex) {
lex.consume('(')
key := reflect.New(v.Type().Key()).Elem()
read(lex, key)
value := reflect.New(v.Type().Elem()).Elem() read(lex,
value)
v.SetMapIndex(chiave, valore)
lex.consume(')')
}

predefinito:
panic(fmt.Sprintf("cannot decode list into %v", v.Type())
}
}

www.it-ebooks.info
SEZIONE 12.6. ESEMPIO: DECODIFICA DELLE ESPRESSIONI S 347

func endList(lex *lexer) bool {


switch lex.token {
case scanner.EOF: panic("fine
del file")
caso ')':
restituire vero
}
restituire false
}

Infine, il parser è racchiuso in una funzione esportata, Unmarshal, mostrata di seguito, che nasconde
alcune delle asperità dell'implementazione. Gli errori riscontrati durante il parsing provocano un panico,
quindi Unmarshal utilizza una chiamata differita per riprendersi dal panico (§5.10) e restituire invece un
messaggio di errore.
// Unmarshal analizza i dati dell'espressione S e popola la variabile
// il cui indirizzo si trova nel puntatore non nullo out.
func Unmarshal(data []byte, out interface{}) (err error) {
lex := &lexer{scan: scanner.Scanner{Mode: scanner.GoTokens}}
lex.scan.Init(bytes.NewReader(data))
lex.next() // ottenere il primo token
defer func() {
// NOTA: questo non è un esempio di gestione ideale degli
errori. if x := recover(); x := nil {
err = fmt.Errorf("errore a %s: %v", lex.scan.Position, x)
}
}()
read(lex, reflect.ValueOf(out).Elem()) return
nil
}

Un'implementazione di qualità non dovrebbe mai andare in panico per nessun input e dovrebbe
riportare un errore informativo per ogni errore, magari con un numero di riga o un offset. Tuttavia,
speriamo che questo esempio trasmetta un'idea di ciò che accade sotto il cofano di pacchetti come
encoding/json e di come si possa usare la riflessione per popolare le strutture dati.

Esercizio 12.8: La funzione sexpr.Unmarshal, come json.Marshal, richiede l'input completo in una
fetta di byte prima di poter iniziare la decodifica. Definire un tipo sexpr.Decoder che, come
json.Decoder, permetta di decodificare una sequenza di valori da un io.Reader. Modificare
sexpr.Unmarshal per utilizzare questo nuovo tipo.

Esercizio 12.9: Scrivere un'API basata sui token per decodificare le espressioni S, seguendo lo stile
di xml.Decoder (§7.14). Sono necessari cinque tipi di token: Symbol, String, Int, StartList e EndList.

Esercizio 12.10: Estendere sexpr.Unmarshal per gestire i booleani, i numeri a virgola mobile e le interfacce
codificate dalla soluzione all'Esercizio 12.3. (Suggerimento: per decodificare le interfacce, è necessaria
una mappatura dal nome di ogni tipo supportato al suo reflect.Type).

www.it-ebooks.info
348 CAPITOLO 12. RIFLESSIONE

12.7. Accesso ai tag del campo Struct

Nella Sezione 4.5 abbiamo usato i tag struct field per modificare la codifica JSON dei valori delle
strutture Go. Il tag json field ci permette di scegliere nomi di campo alternativi e di sopprimere l'output
dei campi vuoti. In questa sezione vedremo come accedere ai tag di campo utilizzando la reflection.

In un server web, la prima cosa che la maggior parte delle funzioni dei gestori HTTP fa è estrarre i
parametri della richiesta in variabili locali. Definiremo una funzione di utilità, params.Unpack, che utilizza
i tag di campo struct per rendere più comoda la scrittura dei gestori HTTP (§7.7).

Per prima cosa, mostreremo come viene utilizzata. La funzione di ricerca qui sotto è un gestore HTTP.
Definisce una variabile chiamata data di tipo struct anonimo, i cui campi corrispondono ai parametri
della richiesta HTTP. I tag dei campi della struct specificano i nomi dei parametri, che spesso sono brevi
e criptici, dato che lo spazio è prezioso in un URL. La funzione Unpack popola la struct dalla richiesta, in
modo che si possa accedere ai parametri in modo comodo e con un tipo appropriato.

gopl.io/ch12/search
importare "gopl.io/ch12/params"

// search implementa l'endpoint URL /search.


func search(resp http.ResponseWriter, req *http.Request) { var data
struct {
Etichette []stringa `http: "l"`
MaxResults int `http: "max"`
Esattamente bool `http: "x"`
}
data.MaxResults = 10 // impostazione predefinita
if err := params.Unpack(req, &data); err := nil { http.Error(resp,
err.Error(), http.StatusBadRequest) // 400 return
}

// ... il resto del gestore... fmt.Fprintf(resp,


"Ricerca: %+v\n", dati)
}

La funzione Unpack qui sotto fa tre cose. Innanzitutto, chiama req.ParseForm() per analizzare la
richiesta. Successivamente, req.Form contiene tutti i parametri, indipendentemente dal fatto che il
client HTTP abbia utilizzato il metodo di richiesta GET o POST.

Successivamente, Unpack crea una mappatura dal nome effettivo di ogni campo alla variabile per
quel campo. Il nome effettivo può differire dal nome reale se il campo ha un tag. Il metodo Field di
reflect.Type restituisce un reflect.StructField che fornisce informazioni sul tipo di ogni campo,
come il nome, il tipo e il tag opzionale. Il campo Tag è un reflect.StructTag, un tipo di stringa che
fornisce un metodo Get per analizzare ed estrarre la sottostringa di una particolare chiave, come
http:"..." in questo caso.

www.it-ebooks.info
SEZIONE 12.7. ACCESSO AI TAG DEI CAMPI STRUCT 349

gopl.io/ch12/params
// Lo scompattamento popola i campi della struttura puntata da ptr
// dai parametri della richiesta HTTP in req.
func Unpack(req *http.Request, ptr interface{}) error { if err
:= req.ParseForm(); err := nil {
restituire err
}

// Costruisce una mappa di campi con la chiave del nome


effettivo. fields := make(map[string]reflect.Value)
v := reflect.ValueOf(ptr).Elem() // la variabile struct for i := 0;
i < v.NumField(); i++ {
fieldInfo := v.Type().Field(i) // un reflect.StructField tag :=
fieldInfo.Tag // un reflect.StructTag
name := tag.Get("http")
se nome == "" {
name = strings.ToLower(fieldInfo.Name)
}
campi[nome] = v.Campo(i)
}

// Aggiornare il campo struct per ogni parametro della richiesta.


for name, values := range req.Form {
f := campi[nome]
if !f.IsValid() {
continua // ignora i parametri HTTP non riconosciuti
}
per _, valore := intervallo di valori {
if f.Kind() == reflect.Slice {
elem := reflect.New(f.Type().Elem()).Elem() if err
:= populate(elem, value); err := nil {
return fmt.Errorf("%s: %v", name, err)
}
f.Set(reflect.Append(f, elem))
} else {
if err := populate(f, value); err := nil { return
fmt.Errorf("%s: %v", name, err)
}
}
}
}
restituire nil
}

Infine, Unpack itera sulle coppie nome/valore dei parametri HTTP e aggiorna i campi struct
corrispondenti. Ricordiamo che lo stesso nome di parametro può comparire più di una volta. Se ciò
accade, e il campo è una slice, tutti i valori di quel parametro vengono accumulati nella slice. Altrimenti,
il campo viene sovrascritto ripetutamente, in modo che solo l'ultimo valore abbia effetto.

www.it-ebooks.info
350 CAPITOLO 12. RIFLESSIONE

La funzione populate si occupa di impostare un singolo campo v (o un singolo elemento di un campo


slice) da un valore di parametro. Per ora, supporta solo stringhe, numeri interi firmati e booleani. Il
supporto di altri tipi è lasciato come esercizio.
func populate(v reflect.Value, value string) error { switch
v.Kind() {
caso reflect.String: v.SetString(valore)
caso reflect.Int:
i, err := strconv.ParseInt(valore, 10, 64) if err
!= nil {
restituire err
}
v.SetInt(i)
caso reflect.Bool:
b, err := strconv.ParseBool(valore) if
err != nil {
restituire err
}
v.SetBool(b)
predefinito:
return fmt.Errorf("tipo non supportato %s", v.Type())
}
restituire nil
}

Se aggiungiamo il gestore del server a un server Web, questa potrebbe essere una sessione tipica:
$ go build gopl.io/ch12/search
$ ./search &
$ ./fetch 'http://localhost:12345/search' Ricerca:
{Labels:[] MaxResults:10 Exact:false}
$ ./fetch 'http://localhost:12345/search?l=golang&l=programming' Ricerca:
{Labels:[programmazione golang] MaxResults:10 Exact:false}
$ ./fetch 'http://localhost:12345/search?l=golang&l=programming&max=100' Ricerca:
{Labels:[golang programming] MaxResults:100 Exact:false}
$ ./fetch 'http://localhost:12345/search?x=true&l=golang&l=programming' Ricerca:
{Labels:[programmazione golang] MaxResults:10 Exact:true}
$ ./fetch 'http://localhost:12345/search?q=hello&x=123' x:
strconv.ParseBool: parsing "123": sintassi non valida
$ ./fetch 'http://localhost:12345/search?q=hello&max=lots' max:
strconv.ParseInt: parsing "lots": sintassi non valida

Esercizio 12.11: Scrivere la funzione Pack corrispondente. Dato un valore di struttura, Pack deve
restituire un URL che incorpori i valori dei parametri della struttura.
Esercizio 12.12: Estendere la notazione dei tag di campo per esprimere i requisiti di validità dei
parametri. Ad esempio, una stringa deve essere un indirizzo e-mail o un numero di carta di credito
validi, mentre un intero deve essere un codice postale statunitense valido. Modificate Unpack per
verificare questi requisiti.

www.it-ebooks.info
SEZIONE 12.8. VISUALIZZAZIONE DEI METODI DI UN TIPO 351

Esercizio 12.13: Modificare il codificatore di espressioni S (§12.4) e il decodificatore (§12.6) in modo che
rispettino il tag di campo sexpr:"..." in modo simile alla codifica/json (§4.5).

12.8. Visualizzazione dei metodi di un tipo

Il nostro ultimo esempio di riflessione utilizza reflect.Type per stampare il tipo di un valore arbitrario ed
enumerare i suoi metodi:

gopl.io/ch12/metodi
// Print stampa l'insieme dei metodi del valore x. func
Print(x interface{}) {
v := reflect.ValueOf(x) t
:= v.Type()
fmt.Printf("tipo %s\n", t)

per i := 0; i < v.NumMethod(); i++ { methType


:= v.Method(i).Type()
fmt.Printf("func (%s) %s%s\n", t, t.Method(i).Name,
strings.TrimPrefix(methType.String(), "func")
}
}

Sia reflect.Type che reflect.Value hanno un metodo chiamato Method. Ogni chiamata
t.Method(i) restituisce un'istanza di reflect.Method, un tipo di struct che descrive il nome e il tipo di un
singolo metodo. Ogni chiamata a v.Method(i) restituisce un reflect.Value che rappresenta un
valore di metodo (§6.4), cioè un metodo legato al suo destinatario. Utilizzando il metodo
reflect.Value.Call (che non abbiamo lo spazio per mostrare qui), è possibile chiamare valori di tipo Func
come questo, ma questo programma ha bisogno solo del suo Type.

Ecco i metodi appartenenti a due tipi, time.Duration e *strings.Replacer:

methods.Print(time.Hour)
// Uscita:
// tipo time.Duration
// func (time.Duration) Hours() float64
// func (time.Duration) Minutes() float64
// func (time.Duration) Nanosecondi() int64
// func (time.Duration) Secondi() float64
// func (time.Duration) String() string

methods.Print(new(strings.Replacer))
// Uscita:
// tipo *strings.Replacer
// func (*strings.Replacer) Replace(stringa) stringa
// func (*strings.Replacer) WriteString(io.Writer, string) (int, error)

www.it-ebooks.info
352 CAPITOLO 12. RIFLESSIONE

12.9. Una parola di cautela su

Le API di riflessione sono molte di più di quelle che abbiamo lo spazio per mostrare, ma gli esempi
precedenti danno un'idea di ciò che è possibile fare. La riflessione è uno strumento potente ed
espressivo, ma deve essere usato con attenzione, per tre motivi.
Il primo motivo è che il codice basato sulla riflessione può essere fragile. Per ogni errore che potrebbe
indurre il compilatore a segnalare un errore di tipo, esiste un modo corrispondente di usare male la
riflessione, ma mentre il compilatore segnala l'errore in fase di compilazione, un errore di riflessione
viene segnalato durante l'esecuzione come panico, possibilmente molto tempo dopo che il programma è
stato scritto o addirittura molto tempo dopo che ha iniziato la sua esecuzione.
Se la funzione readList (§12.6), ad esempio, dovesse leggere una stringa dall'input mentre
popola una variabile di tipo int, la chiamata a reflect.Value.SetString andrebbe in panico.
La maggior parte dei programmi che utilizzano la riflessione presenta rischi simili e occorre prestare
molta attenzione a tenere traccia del tipo, dell'indirizzabilità e dell'impostabilità di ogni
reflect.Value.
Il modo migliore per evitare questa fragilità è assicurarsi che l'uso della riflessione sia completamente
incapsulato all'interno del pacchetto e, se possibile, evitare reflect.Value a favore di tipi specifici
nell'API del pacchetto, per limitare gli input ai valori legali. Se ciò non è possibile, eseguire controlli
dinamici aggiuntivi prima di ogni operazione rischiosa. Come esempio dalla libreria standard, quando
fmt.Printf applica un verbo a un operando non appropriato, non va in panico misteriosamente, ma
stampa un messaggio di errore informativo. Il programma ha ancora un bug, ma è più facile da
diagnosticare.
fmt.Printf("%d %s\n", "hello", 42) // "%!d(stringa=hello) %!s(int=42)"

La riflessione riduce anche la sicurezza e l'accuratezza degli strumenti automatici di refactoring e di


analisi, perché non possono determinare o fare affidamento sulle informazioni sul tipo.
La seconda ragione per evitare la riflessione è che, poiché i tipi servono come forma di documentazione
e le operazioni di riflessione non possono essere soggette al controllo statico dei tipi, il codice fortemente
riflessivo è spesso difficile da capire. Documentate sempre con attenzione i tipi attesi e altri invarianti
delle funzioni che accettano un'interfaccia{} o un reflect.Value.
Il terzo motivo è che le funzioni basate sulla riflessione possono essere più lente di uno o due ordini di
grandezza rispetto al codice specializzato per un particolare tipo. In un programma tipico, la maggior
parte delle funzioni non è rilevante per le prestazioni complessive, quindi va bene usare la riflessione
quando rende il programma più chiaro. I test sono particolarmente adatti alla riflessione, poiché la
maggior parte dei test utilizza insiemi di dati di piccole dimensioni. Ma per le funzioni sul percorso
critico è meglio evitare la riflessione.

www.it-ebooks.info
13
Programmazione a basso
livello
Il progetto di Go garantisce una serie di proprietà di sicurezza che limitano i modi in cui un programma
Go può ''andare storto''. Durante la compilazione, il controllo dei tipi rileva la maggior parte dei tentativi
di applicare un'operazione a un valore che non è appropriato per il suo tipo, ad esempio sottraendo una
stringa da un'altra. Regole rigorose per le conversioni di tipo impediscono l'accesso diretto agli interni
dei tipi incorporati come stringhe, mappe, slices e canali.
Per gli errori che non possono essere rilevati staticamente, come gli accessi ad array fuori limite o le
dereferenze di puntatori nulli, i controlli dinamici assicurano che il programma termini
immediatamente con un errore informativo ogni volta che si verifica un'operazione vietata. La gestione
automatica della memoria (garbage collection) elimina i bug "use after free" e la maggior parte delle
perdite di memoria.
Molti dettagli di implementazione sono inaccessibili ai programmi Go. Non c'è modo di scoprire la
disposizione della memoria di un tipo aggregato come una struct, o il codice macchina di una funzione,
o l' identità del thread del sistema operativo su cui è in esecuzione la goroutine corrente. Infatti, lo
scheduler di Go sposta liberamente le goroutine da un thread all'altro. Un puntatore identifica una
variabile senza rivelarne l'indirizzo numerico. Gli indirizzi possono cambiare quando il garbage collector
sposta le variabili; i puntatori vengono aggiornati in modo trasparente.
L'insieme di queste caratteristiche rende i programmi Go, in particolare quelli che falliscono, più
prevedibili e meno misteriosi dei programmi in C, il linguaggio di basso livello per eccellenza.
Nascondendo i dettagli nascosti, rendono i programmi Go altamente portabili, poiché la semantica del
linguaggio è in gran parte indipendente da ogni particolare compilatore, sistema operativo o architettura
della CPU. (Non del tutto indipendente: alcuni dettagli trapelano, come la dimensione delle parole del
processore, l'ordine di valutazione di alcune espressioni e l'insieme delle restrizioni di implementazione
imposte dal compilatore).
Occasionalmente, si può scegliere di rinunciare ad alcune di queste utili garanzie per ottenere le massime
prestazioni possibili, per interoperare con librerie scritte in altri linguaggi o per implementare una
funzione che non può essere espressa in Go puro.

353

www.it-ebooks.info
354 CAPITOLO 13. PROGRAMMAZIONE A BASSO
LIVELLO

In questo capitolo vedremo come il pacchetto unsafe ci permetta di uscire dalle regole consuete e come
utilizzare lo strumento cgo per creare binding Go per librerie C e chiamate del sistema operativo.
Gli approcci descritti in questo capitolo non devono essere usati in modo frivolo. Senza un'attenta cura
dei dettagli, possono causare i tipi di guasti imprevedibili, imperscrutabili e non locali che i
programmatori C conoscono bene. L'uso di unsafe annulla anche la garanzia di compatibilità di Go con
le versioni future, poiché, sia che sia intenzionale sia che sia involontario, è facile dipendere da dettagli di
implementazione non specificati che possono cambiare inaspettatamente.
Il pacchetto unsafe è piuttosto magico. Sebbene appaia come un normale pacchetto e venga importato nel
modo consueto, in realtà è implementato dal compilatore. Fornisce l'accesso a una serie di funzionalità
integrate nel linguaggio che non sono normalmente disponibili perché espongono dettagli della
disposizione della memoria di Go. Presentare queste funzionalità come un pacchetto separato rende più
evidenti le rare occasioni in cui sono necessarie. Inoltre, alcuni ambienti possono limitare l'uso del
pacchetto unsafe per motivi di sicurezza.
Il pacchetto unsafe è utilizzato ampiamente all'interno di pacchetti di basso livello come runtime, os,
syscall e
che interagiscono con il sistema operativo, ma non è quasi mai necessario per i programmi ordinari.

13.1. unsafe.Sizeof, Alignof e Offsetof

La funzione unsafe.Sizeof riporta la dimensione in byte della rappresentazione del suo operando, che
può essere un'espressione di qualsiasi tipo; l'espressione non viene valutata. Una chiamata a Sizeof è
un'espressione costante di tipo uintptr, quindi il risultato può essere usato come dimensione di un tipo
di array o per calcolare altre costanti.
importare "unsafe"
fmt.Println(unsafe.Sizeof(float64(0))) // "8"

Sizeof riporta solo la dimensione della parte fissa di ogni struttura dati, come il puntatore e la lunghezza
di una stringa, ma non le parti indirette come il contenuto della stringa. Di seguito sono riportate le
dimensioni tipiche di tutti i tipi di Go non aggregati, anche se le dimensioni esatte possono variare a
seconda della toolchain. Per la portabilità, abbiamo indicato le dimensioni dei tipi di riferimento (o dei
tipi che contengono riferimenti) in termini di parole, dove una parola corrisponde a 4 byte su una
piattaforma a 32 bit e a 8 byte su una piattaforma a 64 bit.
I computer caricano e memorizzano i valori dalla memoria in modo più efficiente quando
questi valori sono allineati correttamente. Ad esempio, l'indirizzo di un valore a due byte come int16
dovrebbe essere un numero pari, l'indirizzo di un valore a quattro byte come una runa dovrebbe essere
un multiplo di quattro e l'indirizzo di un valore a otto byte come float64, uint64 o un puntatore a
64 bit dovrebbe essere un multiplo di otto. I requisiti di allineamento di multipli superiori sono insoliti,
anche per i tipi di dati più grandi come complex128.
Per questo motivo, la dimensione di un valore di tipo aggregato (una struct o un array) è almeno la
somma delle dimensioni dei suoi campi o elementi, ma può essere maggiore a causa della presenza di
''buchi''. I buchi sono spazi inutilizzati aggiunti dal compilatore per garantire che il campo o l'elemento
successivo sia allineato correttamente rispetto all'inizio della struttura o dell'array.

www.it-ebooks.info
SEZIONE 13.1. DIMENSIONI, ALLINEAMENTO E SFALSAMENTO NON 355
SICURI.

Tipo Dimensione
bool 1 byte
intN, uintN, floatN, complexN N / 8 byte (ad esempio, float64 è 8 byte)
int, uint, uintptr 1 parola
*T 1 parola
stringa 2 parole (dati, len)
[]T 3 parole (data, len, cap)
mappa 1 parola
func 1 parola
chan 1 parola
interfaccia 2 parole (tipo, valore)

Le specifiche del linguaggio non garantiscono che l'ordine in cui i campi sono dichiarati corrisponda
all'ordine in cui sono disposti in memoria, quindi in teoria un compilatore è libero di riorganizzarli,
anche se al momento in cui scriviamo non lo fa nessuno. Se i tipi dei campi di una struct sono di
dimensioni diverse, può essere più efficiente dal punto di vista dello spazio dichiarare i campi in un
ordine che li impacchetti il più possibile. Le tre struct che seguono hanno gli stessi campi, ma la prima
richiede fino al 50% di memoria in più rispetto alle altre due:
// 64-bit 32 bit
struct{ bool; float64; int16 } // 3 parole 4 parole
struct{ float64; int16; bool } // 2 parole 3 parole
struct{ bool; int16; float64 } // 2 parole 3 parole

I dettagli dell'algoritmo di allineamento esulano dallo scopo di questo libro e non vale certo la pena di
preoccuparsi di ogni struttura, ma un impacchettamento efficiente può rendere più compatte e quindi
più veloci le strutture di dati allocate di frequente.

La funzione unsafe.Alignof riporta l'allineamento richiesto del tipo del suo argomento. Come Sizeof,
può essere applicata a un'espressione di qualsiasi tipo e restituisce una costante. In genere, i tipi booleani
e numerici sono allineati alla loro dimensione (fino a un massimo di 8 byte) e tutti gli altri tipi sono
allineati alle parole.

La funzione unsafe.Offsetof, il cui operando deve essere un selettore di campo x.f, calcola
l'offset del campo f rispetto all'inizio della struttura x che lo racchiude, tenendo conto di eventuali buchi.

La Figura 13.1 mostra una variabile struct x e la sua disposizione in memoria su tipiche implementazioni
Go a 32 e 64 bit. Le regioni grigie sono buchi.
var x struct {
a bool
b int16
c []int
}

La tabella seguente mostra i risultati dell'applicazione delle tre funzioni non sicure a x stessa e a ciascuno
dei suoi tre campi:

www.it-ebooks.info
356 CAPITOLO 13. PROGRAMMAZIONE A BASSO
LIVELLO

Figura 13.1. Fori in una struttura.

Piattaforma tipica a 32 bit:


Dimensione(x) = 16 Alignof(x) = 4
Sizeof(x.a) = 1 Alignof(x.a) = 1 Offsetof(x.a) = 0
Sizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2
Sizeof(x.c) = 12 Alignof(x.c) = 4 Offsetof(x.c) = 4

Tipica piattaforma a 64 bit:


Dimensione(x) = 32 Alignof(x) = 8
Dimensione(x.a) = 1 Alignof(x.a) = 1 Offsetof(x.a) = 0
Dimensione(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2
Sizeof(x.c) = 24 Alignof(x.c) = 8 Offsetof(x.c) = 8

Nonostante i loro nomi, queste funzioni non sono in realtà non sicure e possono essere utili per capire la
disposizione della memoria grezza in un programma quando si ottimizza lo spazio.

13.2. unsafe.Pointer

La maggior parte dei tipi di puntatore si scrive *T, che significa ''un puntatore a una
variabile di tipo T''. Il tipo unsafe.Pointer è un tipo speciale di puntatore che può contenere l'indirizzo
di qualsiasi variabile. Naturalmente, non possiamo indirettamente passare attraverso un
unsafe.Pointer usando *p perché non sappiamo che tipo dovrebbe avere quell'espressione. Come i
puntatori ordinari, i puntatori unsafe.Pointer sono comparabili e possono essere confrontati con nil,
che è il valore zero del tipo.

Un puntatore *T ordinario può essere convertito in un puntatore unsafe.Pointer e un puntatore


unsafe.Pointer può essere riconvertito in un puntatore ordinario, non necessariamente dello stesso tipo
*T. Con-vertendo un puntatore *float64 in un *uint64, ad esempio, si può ispezionare il modello di bit di
una variabile in virgola mobile:
pacchetto matematica

func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) }

fmt.Printf("%#016x\n", Float64bits(1.0)) // "0x3ff0000000000000"

www.it-ebooks.info
SEZIONE 13.2. PUNTO NON SICURO 357

Attraverso il puntatore risultante, possiamo aggiornare anche il modello di bit. Questo è innocuo per
una variabile in virgola mobile, poiché qualsiasi modello di bit è legale, ma in generale non è sicuro. Le
conversioni dei puntatori ci permettono di scrivere valori arbitrari in memoria e quindi di sovvertire il
sistema dei tipi.

Un puntatore unsafe.Pointer può anche essere convertito in un uintptr che contiene il valore numerico
del puntatore, consentendoci di eseguire aritmetica sugli indirizzi. (Ricordiamo dal Capitolo 3 che un
uintptr è un intero senza segno sufficientemente ampio da rappresentare un indirizzo). Questa
conversione può essere applicata anche al contrario, ma anche in questo caso la conversione da un
uintptr a un unsafe.Pointer può sovvertire il sistema dei tipi, poiché non tutti i numeri sono indirizzi
validi.

Molti valori unsafe.Pointer sono quindi intermediari per convertire i puntatori ordinari in indirizzi
numerici grezzi e viceversa. L'esempio che segue prende l'indirizzo della variabile x, aggiunge l'offset del
suo campo b, converte l'indirizzo risultante in *int16 e attraverso quel puntatore aggiorna x.b:
gopl.io/ch13/unsafeptr
var x struct {
a bool
b int16
c []int
}

// equivalente a pb := &x.b
pb := (*int16)(unsafe.Pointer( uintptr(unsafe.Pointer(&x)) +
unsafe.Offsetof(x.b))
*pb = 42

fmt.Println(x.b) // "42"

Sebbene la sintassi sia macchinosa - e forse non è un male, visto che queste funzioni dovrebbero essere
usate con parsimonia - non siate tentati di introdurre variabili temporanee di tipo uintptr per spezzare le
righe. Questo codice non è corretto:
// NOTA: sottilmente scorretto!
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b) pb :=
(*int16)(unsafe.Pointer(tmp))
*pb = 42

Il motivo è molto sottile. Alcuni garbage collector spostano le variabili in memoria per ridurre la
frammentazione o il bookkeeping. I garbage collector di questo tipo sono noti come moving GC. Quando
una variabile viene spostata, tutti i puntatori che contengono l'indirizzo della vecchia posizione devono
essere aggiornati per puntare a q u e l l a nuova. Dal punto di vista del garbage collector, un
unsafe.Pointer è un puntatore e quindi il suo valore deve cambiare quando la variabile si sposta, ma un
uintptr è solo un numero e quindi il suo valore non deve cambiare. Il codice errato qui sopra nasconde un
puntatore al garbage collector nella variabile non-puntatore tmp. Nel momento in cui viene eseguita la
seconda istruzione, la variabile x potrebbe essersi spostata e il numero in tmp non sarebbe più l'indirizzo
&x.b. La terza istruzione blocca una posizione di memoria arbitraria con i l valore 42.

www.it-ebooks.info
358 CAPITOLO 13. PROGRAMMAZIONE A BASSO
LIVELLO

Esistono miriadi di variazioni patologiche su questo tema. Dopo l'esecuzione di questa dichiarazione:
pT := uintptr(unsafe.Pointer(new(T))) // NOTA: sbagliato!

non ci sono puntatori che si riferiscono alla variabile creata da new, quindi il garbage collector è
autorizzato a riciclare la sua memoria al termine di questa istruzione, dopo di che pT contiene l'indirizzo
in cui la variabile si trovava ma non è più.
Nessuna implementazione attuale di Go utilizza un garbage collector mobile (anche se le
implementazioni future potrebbero farlo), ma questo non è un motivo di compiacimento: le versioni
attuali di Go spostano alcune variabili in memoria. Ricordiamo dalla Sezione 5.2 che gli stack delle
goroutine crescono secondo le necessità. Quando ciò accade, tutte le variabili presenti nel vecchio stack
possono essere ricollocate in un nuovo stack più grande, quindi non possiamo contare sul fatto che il
valore numerico dell'indirizzo di una variabile rimanga invariato per tutta la sua durata.
Al momento della stesura di questo articolo, non ci sono indicazioni chiare su cosa i programmatori di
Go possano fare affidamento dopo una conversione da unsafe.Pointer a uintptr (si veda il numero 7192
di Go), quindi si raccomanda vivamente di assumere il minimo indispensabile. Trattate tutti i valori
uintptr come se contenessero l'indirizzo precedente di una variabile e riducete al minimo il numero di
operazioni tra la conversione di un unsafe.Pointer in un uintptr e l'utilizzo di tale uintptr. Nel primo
esempio, le tre operazioni - la conversione in uintptr, l'aggiunta dell'offset del campo e la conversione -
compaiono tutte in una singola espressione.
Quando si chiama una funzione di libreria che restituisce un uintptr, come quelle di seguito riportate dal
pacchetto reflect, il risultato deve essere immediatamente convertito in un unsafe.Pointer per garantire che
continui a puntare alla stessa variabile.
pacchetto riflettere
func (Valore) Puntatore() uintptr func
(Valore) UnsafeAddr() uintptr
func (Valore) InterfaceData() [2]uintptr // (indice 1)

13.3. Esempio: Equivalenza profonda

La funzione DeepEqual del pacchetto reflect indica se due valori sono ''profondamente'' uguali.
DeepEqual confronta i valori di base come con l'operatore built-in ==; per i v a l o r i composti, li
attraversa ricorsivamente, confrontando gli elementi corrispondenti. Poiché funziona per qualsiasi
coppia di valori, anche per quelli non confrontabili con ==, trova largo impiego nei test. Il test seguente
utilizza DeepEqual per confrontare due valori []stringa:
func TestSplit(t *testing.T) {
got := strings.Split("a:b:c", ":")
want := []string{"a", "b", "c"};
if !reflect.DeepEqual(got, want) { /* ... */ }
}

Sebbene DeepEqual sia comodo, le sue distinzioni possono sembrare arbitrarie. Ad esempio, non considera
una mappa nil uguale a una mappa vuota non nil, né una slice nil uguale a una slice vuota non nil:

www.it-ebooks.info
SEZIONE 13.3. ESEMPIO: EQUIVALENZA PROFONDA 359

var a, b []string = nil, []string{}


fmt.Println(reflect.DeepEqual(a, b)) // "falso"

var c, d map[string]int = nil, make(map[string]int) fmt.Println(reflect.DeepEqual(c, d)) //


"falso"

In questa sezione definiremo una funzione Equal che confronta valori arbitrari. Come DeepEqual,
confronta le fette e le mappe in base ai loro elementi, ma a differenza di DeepEqual, considera una fetta (o
una mappa) nulla uguale a una vuota non nulla. La ricorsione di base sugli argomenti può essere
eseguita con la riflessione, utilizzando un approccio simile a quello del programma Display visto nella
Sezione 12.3. Come al solito, definiamo un'istruzione non esente da errori. Come al solito, definiamo
una funzione non esportata, equal, per la ricorsione. Non preoccupatevi ancora del parametro seen.
Per ogni coppia di valori x e y da confrontare, equal verifica che entrambi (o nessuno dei due)
siano validi e controlla che abbiano lo stesso tipo. Il risultato della funzione è definito come un insieme
di casi di switch che confrontano due valori dello stesso tipo. Per motivi di spazio, abbiamo omesso
alcuni casi, poiché lo schema dovrebbe essere ormai familiare.
gopl.io/ch13/equal
func equal(x, y reflect.Value, seen map[comparison]bool) bool { if
!x.IsValid() || !y.IsValid() {
return x.IsValid() == y.IsValid()
}
if x.Type() != y.Type() {
return false
}

// ...controllo del ciclo omesso (mostrato più

avanti)... switch x.Kind() {


caso reflect.Bool:
restituire x.Bool() == y.Bool()

case reflect.String:
restituire x.String() == y.String()

// ...casi numerici omessi per brevità...

case reflect.Chan, reflect.UnsafePointer, reflect.Func: return


x.Pointer() == y.Pointer()

case reflect.Ptr, reflect.Interface: return


equal(x.Elem(), y.Elem(), seen)

case reflect.Array, reflect.Slice: if


x.Len() != y.Len() {
restituire false
}
per i := 0; i < x.Len(); i++ {
if !equal(x.Index(i), y.Index(i), seen) { return
false
}
}
restituire vero

www.it-ebooks.info
360 CAPITOLO 13. PROGRAMMAZIONE A BASSO
LIVELLO

// ...casi di struct e map omessi per brevità...


}
panic("irraggiungibile")
}

Come al solito, non esponiamo l'uso della riflessione nell'API, quindi la funzione esportata Equal
deve chiamare reflect.ValueOf sui suoi argomenti:
// Equal indica se x e y sono profondamente uguali. func
Equal(x, y interface{}) bool {
seen := make(map[comparison]bool)
restituire equal(reflect.ValueOf(x), reflect.ValueOf(y), seen)
}

type comparison struct { x,


y unsafe.Pointer t
reflect.Type
}

Per garantire che l'algoritmo termini anche per le strutture dati cicliche, deve registrare quali coppie di
variabili ha già confrontato ed evitare di confrontarle una seconda volta. Equal alloca una serie di
strutture di confronto, ognuna delle quali contiene l'indirizzo di due variabili (rappresentate come valori
unsafe.Pointer) e il tipo di confronto. Oltre agli indirizzi, è necessario registrare anche il tipo, perché
variabili diverse possono avere lo stesso indirizzo. Ad esempio, se x e y sono entrambi array, x e x[0]
hanno lo stesso indirizzo, così come y e y[0], ed è importante distinguere se abbiamo confrontato x e
y o x[0] e y[0].

Una volta che equal ha stabilito che i suoi argomenti hanno lo stesso tipo e prima di eseguire lo switch,
controlla se sta confrontando due variabili già viste e, in tal caso, termina la ricorsione.
// controllo del ciclo
se x.CanAddr() && y.CanAddr() {
xptr := unsafe.Pointer(x.UnsafeAddr()) yptr
:= unsafe.Pointer(y.UnsafeAddr()) if xptr ==
yptr {
return true // riferimenti identici
}
c := confronto{xptr, yptr, x.Type()} if
seen[c] {
return true // già visto
}
seen[c] = true
}

Ecco la funzione Equal in azione:


fmt.Println(Equal([]int{1, 2, 3}, []int{1, 2, 3})) // "vero"
fmt.Println(Equal([]string{"pippo"}, []string{"bar"})) // "falso"
fmt.Println(Equal([]stringa(nil), []stringa{})) // "vero"
fmt.Println(Equal(map[string]int(nil), map[string]int{})) // "vero"

www.it-ebooks.info
SEZIONE 13.4. CHIAMATA DEL CODICE C CON 361
CGO

Funziona anche con gli ingressi ciclici, come quello che ha causato il blocco della funzione Display della
Sezione 12.3:
// Liste collegate circolari a -> b -> a e c -> c. type link
struct {
valore stringa
coda *link
}
a, b, c := &link{valore: "a"}, &link{valore: "b"}, &link{valore: "c"} a.tail, b.tail,
c.tail = b, a, c
fmt.Println(Equal(a, a)) // "vero"
fmt.Println(Equal(b, b)) // "vero"
fmt.Println(Equal(c, c)) // "vero"
fmt.Println(Equal(a, b)) // "falso"
fmt.Println(Equal(a, c)) // "falso"

Esercizio 13.1: Definire una funzione di confronto profondo che consideri i numeri (di qualsiasi tipo)
uguali se differiscono di meno di una parte su un miliardo.
Esercizio 13.2: Scrivete una funzione che segnali se il suo argomento è una struttura di dati ciclica.

13.4. Chiamare il codice C con cgo

Un programma Go potrebbe dover utilizzare un driver hardware implementato in C, interrogare un


database incorporato implementato in C++ o utilizzare alcune routine di algebra lineare implementate
in Fortran. Il C è stato a lungo la lingua franca della programmazione, quindi molti pacchetti destinati a
un uso diffuso esportano un'API compatibile con il C, indipendentemente dalla lingua di
implementazione.
In questa sezione, costruiremo un semplice programma di compressione dei dati che utilizza cgo, uno
strumento che crea binding di Go per funzioni C. Tali strumenti sono chiamati foreign-function
interfaces (FFI) e cgo non è l'unico per i programmi Go. SWIG (swig.org) è un altro; fornisce funzioni
più complesse per l'integrazione con le classi C++, ma non lo mostreremo qui.
Il sottoalbero compress/... della libreria standard fornisce compressori e decompressori per i più diffusi
algoritmi di compressione, tra cui LZW (usato dal comando Unix compress) e DEFLATE (usato dal
comando GNU gzip). Le API di questi pacchetti variano leggermente nei dettagli, ma tutti forniscono un
wrapper per un io.Writer che comprime i dati scritti su di esso e un wrapper per un io.Reader che
decomprime i dati letti da esso. Ad esempio:
pacchetto gzip // compress/gzip
func NewWriter(w io.Writer) io.WriteCloser
func NewReader(r io.Reader) (io.ReadCloser, error)

L'algoritmo bzip2, che si basa sull'elegante trasformazione Burrows-Wheeler, è più lento di gzip ma
offre una compressione significativamente migliore. Il pacchetto compress/bzip2 fornisce un
decompressore per bzip2, ma al momento non fornisce alcun compressore. Costruirne uno da zero è
un'impresa non da poco, ma esiste un'implementazione open-source in C ben documentata e di alta
qualità, il pacchetto libbzip2 di bzip.org.

www.it-ebooks.info
362 CAPITOLO 13. PROGRAMMAZIONE A BASSO
LIVELLO

Se la libreria C fosse piccola, basterebbe portarla in Go puro e se le sue prestazioni non fossero critiche
per i nostri scopi, sarebbe meglio invocare un programma C come sottoprocesso ausiliario usando il
pacchetto os/exec. È quando si ha bisogno di usare una libreria complessa e critica per le prestazioni con
un'API C ristretta che può avere senso avvolgerla con cgo. Per il resto del capitolo, vedremo un esempio.

Dal pacchetto C libbzip2, abbiamo bisogno del tipo struct bz_stream, che contiene i buffer di ingresso e di
uscita, e di tre funzioni C: BZ2_bzCompressInit, che alloca i buffer dello stream; BZ2_bzCompress, che
comprime i dati dal buffer di ingresso a quello di uscita; e BZ2_bzCompressEnd, che rilascia i buffer. (Non
preoccupatevi della meccanica del pacchetto libbzip2; lo scopo di questo esempio è mostrare come le parti
si integrano).

Chiameremo le funzioni C BZ2_bzCompressInit e BZ2_bzCompressEnd direttamente da Go, ma per


BZ2_bzCompress definiremo una funzione wrapper in C, per mostrare come si fa. Il file sorgente C qui
sotto si trova accanto al codice Go nel nostro pacchetto:

gopl.io/ch13/bzip
/* Questo file è gopl.io/ch13/bzip/bzip2.c, */
/* un semplice wrapper per libbzip2 adatto a cgo. */ #includere
<bzlib.h>

int bz2compress(bz_stream *s, int action,


char *in, unsigned *inlen, char *out, unsigned *outlen) { s-
>next_in = in;
s->avail_in = *inlen; s-
>next_out = out;
s->avail_out = *outlen;
int r = BZ2_bzCompress(s, azione);
*inlen -= s->avail_in;
*outlen -= s->avail_out; return
r;
}

Passiamo ora al codice Go, la cui prima parte è mostrata di seguito. La dichiarazione import "C" è
speciale. Non esiste un pacchetto C, ma questa importazione fa sì che go build preprocessi il file usando
lo strumento cgo prima che il compilatore Go lo veda.

// Il pacchetto bzip fornisce uno scrittore che utilizza la compressione bzip2 (bzip.org).
pacchetto bzip

/*
#cgo CFLAGS: -I/usr/include #cgo
LDFLAGS: -L/usr/lib -lbz2 #include
<bzlib.h>
int bz2compress(bz_stream *s, int action,
char *in, unsigned *inlen, char *out, unsigned *outlen);
*/
importare "C"

www.it-ebooks.info
SEZIONE 13.4. CHIAMATA DEL CODICE C CON 363
CGO

importare (
"io"
"unsafe"
)

tipo scrittore struct {


w io.Writer // flusso di output sottostante flusso
*C.bz_stream
outbuf [64 * 1024]byte
}

// NewWriter restituisce uno scrittore per flussi compressi bzip2. func


NewWriter(out io.Writer) io.WriteCloser {
const (
blockSize = 9
verbosità = 0
fattore di lavoro = 30
)
w := &scrittore{w: out, stream: new(C.bz_stream)} C.BZ2_bzCompressInit(w.stream,
blockSize, verbosity, workFactor) return w
}

Durante la preelaborazione, cgo genera un pacchetto temporaneo che contiene dichiarazioni Go che
rispondono a tutte le funzioni e i tipi C usati dal file, come C.bz_stream e C.BZ2_bzCompressInit. Lo
strumento cgo scopre questi tipi invocando il compilatore C in modo speciale sul contenuto del
commento che precede la dichiarazione di importazione.

Il commento può contenere anche le direttive #cgo che specificano opzioni aggiuntive per la toolchain C.
I valori CFLAGS e LDFLAGS forniscono argomenti aggiuntivi ai comandi del compilatore e del linker, in
modo che possano localizzare il file di intestazione bzlib.h e la libreria di archivio libbz2.a. L'esempio
presuppone che queste siano installate sotto /usr sul sistema. Potrebbe essere necessario modificare o
eliminare questi flag per la propria installazione.

NewWriter effettua una chiamata alla funzione C BZ2_bzCompressInit per inizializzare i buffer del
flusso. Il tipo di writer include un altro buffer che verrà usato per svuotare il buffer di uscita del
decom- pressore.

Il metodo Write, mostrato di seguito, invia i dati non compressi al compressore, richiamando la
funzione bz2compress in un ciclo finché tutti i dati non sono stati consumati. Si noti che il
programma Go può accedere a tipi C come bz_stream, char e uint, a funzioni C come bz2compress e
persino a macro del preprocessore C simili a oggetti come BZ_RUN, il tutto attraverso la notazione C.x. Il
tipo C.uint è distinto dal tipo uint di Go, anche se entrambi hanno la stessa larghezza.
func (w *scrittore) Scrivi(dati []byte) (int, error) { if
w.stream == nil {
panic("chiuso")
}
var total int // byte scritti non compressi

www.it-ebooks.info
364 CAPITOLO 13. PROGRAMMAZIONE A BASSO
LIVELLO

per len(dati) > 0 {


inlen, outlen := C.uint(len(data)), C.uint(cap(w.outbuf)) C.bz2compress(w.stream,
C.BZ_RUN,
(*C.char)(unsafe.Pointer(&data[0])), &inlen,
(*C.char)(unsafe.Pointer(&w.outbuf)), &outlen)
totale += int(inlen)
dati = dati[inlen:]
if _, err := w.w.Write(w.outbuf[:outlen]); err := nil { return
total, err
}
}
restituire il totale, nil
}

Ogni iterazione del ciclo passa a bz2compress l'indirizzo e la lunghezza della porzione di dati
rimanente e l'indirizzo e la capacità di w.outbuf. Le due variabili di lunghezza vengono passate per i
loro indirizzi, non per i loro valori, in modo che la funzione C possa aggiornarle per indicare quanti dati
non compressi sono stati consumati e quanti dati compressi sono stati prodotti. Ogni pezzo di dati
compressi viene quindi scritto nel sottostante io.Writer.
Il metodo Close ha una struttura simile a quella di Write, utilizzando un ciclo per eliminare i dati
compressi rimanenti dal buffer di uscita dello stream.
// Close scarica i dati compressi e chiude lo stream.
// Non chiude l'io.Writer sottostante. func (w *writer)
Close() error {
if w.stream == nil {
panic("closed")
}
defer func() {
C.BZ2_bzCompressEnd(w.stream)
w.stream = nil
}()
per {
inlen, outlen := C.uint(0), C.uint(cap(w.outbuf))
r := C.bz2compress(w.stream, C.BZ_FINISH, nil, &inlen,
(*C.char)(unsafe.Pointer(&w.outbuf)), &outlen)
if _, err := w.w.Write(w.outbuf[:outlen]); err := nil { return
err
}
se r == C.BZ_STREAM_END {
restituire nil
}
}
}

Al termine, Close chiama C.BZ2_bzCompressEnd per rilasciare i buffer dello stream, usando defer per
assicurarsi che ciò avvenga su tutti i percorsi di ritorno. A questo punto il puntatore w.stream non è
più sicuro da dereferenziare. Per difenderci, lo impostiamo a nil e aggiungiamo controlli espliciti su
nil a ogni metodo, in modo che il programma vada nel panico se l'utente chiama per errore un
metodo dopo Close.

www.it-ebooks.info
SEZIONE 13.4. CHIAMATA DEL CODICE C CON 365
CGO

Non solo Writer non è sicuro dal punto di vista delle concurrency, ma le chiamate concomitanti a Close e
Write potrebbero causare un crash del programma in codice C. La soluzione a questo problema è
l'Esercizio 13.3.
Il programma che segue, bzipper, è un comando di compressione bzip2 che utilizza il nostro nuovo
pacchetto. Si comporta come il comando bzip2 presente su molti sistemi Unix.
gopl.io/ch13/bzipper
// Bzipper legge l'input, lo comprime in bzip2 e lo scrive. pacchetto
main
importare (
"io"
"log"
"os"
"gopl.io/ch13/bzip"
)
func main() {
w := bzip.NewWriter(os.Stdout)
if _, err := io.Copy(w, os.Stdin); err := nil {
log.Fatalf("bzipper: %v\n", err)
}
if err := w.Close(); err := nil { log.Fatalf("bzipper:
chiusura: %v\n", err)
}
}

Nella sessione che segue, usiamo bzipper per comprimere /usr/share/dict/words, il dizionario di
sistema, da 938.848 byte a 335.405 byte, circa un terzo della sua dimensione originale, e poi lo
scomponiamo con il comando di sistema bunzip2. L'hash SHA256 è lo stesso prima e dopo, il che ci
dà la certezza che il compressore funziona correttamente. (Se non avete sha256sum sul vostro
sistema, usate la soluzione all'Esercizio 4.2).
$ go build gopl.io/ch13/bzipper
$ wc -c < /usr/share/dict/words 938848
$ sha256sum < /usr/share/dict/words
126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2f3a8ab1a6ed -
$ ./bzipper < /usr/share/dict/words | wc -c 335405
$ ./bzipper < /usr/share/dict/words | bunzip2 | sha256sum
126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2a8ab1a6ed -

Abbiamo dimostrato il collegamento di una libreria C in un programma Go. In un'altra direzione, è


anche possibile compilare un programma Go come archivio statico che può essere collegato a un
programma C o come libreria condivisa che può essere caricata dinamicamente da un programma C.
Abbiamo solo scalfito la superficie di cgo e c'è molto altro da dire sulla gestione della memoria, i
puntatori, le callback, la gestione dei segnali, le stringhe, gli errno, i finalizzatori e la relazione tra le
goroutine e i thread del sistema operativo, in gran parte molto sottile. In particolare, le regole per passare
correttamente i puntatori da Go a C o viceversa sono complesse, per ragioni analoghe a quelle che
abbiamo illustrato in precedenza.

www.it-ebooks.info
366 CAPITOLO 13. PROGRAMMAZIONE A BASSO
LIVELLO

discusso nella Sezione 13.2 e non ancora specificato in modo autorevole. Per ulteriori letture, iniziare con
https://golang.org/cmd/cgo.
Esercizio 13.3: Usare sync.Mutex per rendere bzip2.writer sicuro per l'uso simultaneo da parte di più
goroutine.
Esercizio 13.4: Dipendere dalle librerie C ha i suoi svantaggi. Fornire un'implementazione alternativa
di bzip.NewWriter che utilizzi il pacchetto os/exec per eseguire /bin/bzip2 come sottoprocesso.

13.5. Un'altra parola di cautela su

Abbiamo concluso il capitolo precedente con un avvertimento sugli aspetti negativi dell'interfaccia
reflection. Questo avvertimento si applica con ancora più forza al pacchetto non sicuro descritto in questo
capitolo.
I linguaggi di alto livello isolano i programmi e i programmatori non solo dalle arcane specificità dei set
di istruzioni dei singoli computer, ma anche dalla dipendenza da fattori irrilevanti come la posizione in
memoria di una variabile, la dimensione di un tipo di dato, i dettagli della disposizione delle strutture e
una serie di altri dettagli di implementazione. Grazie a questo strato isolante, è possibile scrivere
programmi sicuri e robusti, in grado di funzionare su qualsiasi sistema operativo senza alcuna modifica.
Il pacchetto unsafe consente ai programmatori di accedere all'isolamento per utilizzare qualche funzione
cruciale ma altrimenti inaccessibile, o forse per ottenere prestazioni più elevate. Il costo è solitamente la
portabilità e la sicurezza, quindi si usa unsafe a proprio rischio e pericolo. I nostri consigli su come e
quando usare l'unsafe sono paralleli ai commenti di Knuth sull'ottimizzazione prematura, che abbiamo
citato nella Sezione 11.5. La maggior parte dei programmatori non avrà mai bisogno di usare unsafe.
Tuttavia, occasionalmente si verificheranno situazioni in cui una parte critica del codice può essere
scritta al meglio utilizzando unsafe. Se uno studio e una misurazione accurati indicano che l'unsafe è
davvero l'approccio migliore, limitatelo a un'area più piccola possibile, in modo che la maggior parte
del programma sia ignara del suo utilizzo.
Per ora, mettete in secondo piano gli ultimi due capitoli. Scrivete qualche programma Go sostanzioso.
Evitate di riflettere e di non essere sicuri; tornate su questi capitoli solo se necessario.
Nel frattempo, buona programmazione Go. Speriamo che vi piaccia scrivere Go quanto piace a noi.

www.it-ebooks.info
Indice

!, operatore di negazione 63 Notazione sintattica astratta uno campo struct 104, 105, 106, 162
%, operatore di resto 52, 166 (ASN.1) 107 API
&&, operatore AND di cortocircuito 63 tipo astratto 24, 171 codifica 213, 340
&, indirizzo dell'operatore 24, 32, 94, astrazione, prematura 216, 316, 317 errore 127, 152
polimorfismo ad hoc 211 pacchetto 284, 296, 311, 333, 352
158, 167 indirizzo di variabile locale 32, 36
&, implicito 158, 167 tempo di esecuzione 324
indirizzo di struct literal 103 espressione
&^, operatore AND-NOT 53 indirizzabile 159, 341 SQL 211
&^, operatore di cancellazione dei bit 53 valore indirizzabile 32 chiamata di sistema 196
' carattere di citazione 56 indirizzo dell'operatore e 24, 32, 94, modello 115
*, operatore di indirezione 24, 32 158, 167 decodificatore a gettoni 213, 215,
++, istruzione di incremento 5, 37, 94 tipo di aggregato 81, 99 347
+, operatore di concatenazione di Linguaggio di programmazione Alef Linguaggio di programmazione APL xiii
stringhe 5, 65 xiii algoritmo funzione incorporata append 88, 90, 91
+, operatore unario 53 ricerca breadth-first 139, 239 esempio appendInt 88 argomenti
+=, -=, ecc., operatore di assegnazione 5 ricerca depth-first 136 ... 139, 142
-, operatore unario 53 Fibonacci 37, 218 riga di comando 4, 18, 33, 43, 179,
--, dichiarazione di decremento 5, 37 GCD 37 180, 290, 313
... argomento 139, 142 ordinamento di inserimento 101 funzione 119
... lunghezza dell'array 82 Lissajous 15 puntatore 33, 83
... parametro 91, 142, 143, 172 rotazione della fetta 86 fetta 86
... percorso 292, 299 ordinamento topologico 136 valutatore di espressioni aritmetiche 197
/*...*/ commento 5, 25 array
aliasing, puntatore 33
// commento 5, 25 allineamento 354 confronto 83
:= dichiarazione variabile breve 5, 31, assegnazione lunghezza, ... 82
49 cumulo 36 letterale 82, 84
<<, operatore di spostamento a sinistra 54 memoria 36, 71, 89, 169, 209, 322 tipo 81
==, operatore di confronto 40, 63 pila 36 sottostante 84, 88, 91, 187
>>, operatore di spostamento a destra 54 elemento di ancoraggio, HTML 122 valore zero 82
^, operatore di complemento bitwise 53 Operatore AND &&, cortocircuito 63 ASCII 56, 64, 66, 67, 305
^, operatore OR esclusivo 53 Operatore AND-NOT &^ 53 ASN.1 (Notazione sintattica astratta
animazione, GIF 13 uno) 107
_, identificatore in bianco 7, 38, 95, 120, catena di montaggio, torta 234
anonimo
126, asserzioni
funzione 22, 135, 236
287 funzione 316
funzione, rinviare 146
Carattere "backquote" 66 tipo di interfaccia 208, 210
funzione, ricorsiva 137
| Nel modello 113
|, operatore OR bitwise 166, 167
||, operatore OR di cortocircuito 63

367

www.it-ebooks.info
368 INDICE

test 306 dichiarazione di rottura 24, 46 Linguaggio di programmazione C xii,


tipo 205, 211 dichiarazione di rottura, xv, 1, 6, 52, 260, 361
cedibilità 38, 175 etichettata 249 prova di fragilità cache, concorrente non bloccante 272 cache,
assegnabilità, interfaccia 175 assegnazione 317 non bloccante 275
trasmissione 251, 254, 276 catena di montaggio delle
implicito 38 Brooks, Fred xiv torte 234 chiamata
operatore a valore multiplo
funzione btoi 64 con riferimento 83
37 +=, -=, ecc. 5 operatori 36,
52 canale bufferizzato 226, 231 per valore 83, 120, 158
pacchetto bufio 9 funzione metodo dell'interfaccia 182
dichiarazione 5, 7, 36, 52, 94, 173
bufio.NewReader 98 funzione valore ok dalla funzione 128 chiamata
tupla 31, 37 bufio.NewScanner 9 C da Go 361
associatività, operatore 52 (*bufio.Reader).ReadRune caso cammello 28
operazione atomica 264 attacco, metodo 98
iniezione HTML 115 attacco, annullamento 251, 252
bufio.Scanner tipo 9 annullamento della richiesta HTTP 253
iniezione SQL 211 esempio di
autoescape 117 (*bufio.Scanner).Err metodo 97 funzione integrata del cappuccio 84, 232
(*bufio.Scanner). Metodo di capacità, canale 226, 232, 233
porta posteriore, pacchetto 315 scansione 9 capacità, fetta 88, 89
back-off, esponenziale 130 (*bufio.Scanner).Split metodo 99 catturare la variabile di iterazione 140
carattere backquote, ` 66 funzione bufio.ScanWords 99 catturare la variabile di ciclo 141, 236,
pacchetto di esempio bancario 258, 261, +commenti di costruzione 296 240
263 vincoli di costruzione 296 caso in tipo switch 212
ritorno nudo 126 costruire tag 296 caso, selezionare 245
nome base esempio 72 costruzione di pacchetti Tipo Celsius 39
293 funzione integrata
comportamento, non definito 260 Funzione CelsiusFlag 181
Funzione di benchmark 302, 321 da append 88, 90, 91
cfr. esempio 43
bidirezionale a unidirezionale cap 84, 232
strumento cgo 361, 362
conversione canale 231 chiudere 226, 228, 251
<-ch, ricezione canale 18, 225, 232
binario complesso 61
ch<-, invio canale 18, 225, 232
operatori, tabella di 52 copia 89
semaforo 262 concatenamento, metodo 114
eliminare 94 chan tipo 225 canale
albero 102 immagina 61
vettore di bit 165 tamponato 226, 231
len 4, 54, 64, 65, 81, 84, 233 capacità 226, 232, 233
operatore bit-clear &^ 53
fare 9, 18, 88, 94, 225 chiudere 228, 251
tipo di dati bit-set 77
bitwise nuovo 34 chiusura di un 225
operatore di complemento ^ 53 panico 148, 149 comunicazione 225, 245
operatori, tabella di 53 reale 61 confronto 225 conversione,
Operatore OR | 166, 167 recuperare 152 bidirezionale a
test black-box 310 interfaccia integrata, errore 196 unidirezionale 231
Identificatore vuoto _ 7, 38, 95, 120, 126, tipo integrato, errore 11, 128, 149, drenaggio di un 229, 252
287 196 fare 18, 225
blocco vuoto di Conversione da fetta di byte a stringa 73 zero 246, 249
importazione 287 byte tipo 52 sondaggio 246
file 46 Esempio di ByteCounter 173 gamma oltre 229
lessicale 46, 120, 135, 141, 212 pacchetto di byte 71, 73 ricevere <-ch 18, 225, 232
locale 46 bytes.Tipo di buffer 74, 169, 172, 185 ricezione, non bloccante 246
pacchetto 46 (*bytes.Buffer).Grow metodo 169 ricezione, valore ok da 229
universo 46 (*bytes.Buffer).WriteByte invio ch<- 18, 225, 232
profilo di blocco 324 Blog, metodo 74 sincrono 226
Go xvi, 326 esempio di (*bytes.Buffer).WriteRune tipo 18
bollitura 29 metodo 74 tipo <-chan T, solo ricezione 230 tipo
bool tipo 63 booleano (*bytes.Buffer).WriteString chan<- T, solo invio 230 tipo,
costante, falso 63 metodo 74 unidirezionale 230, 231
costante, vero 63 funzione bytes.Equal 86 non bufferizzato 226
valore zero 30 bzip Codice C 362 valore zero 225, 246
funzione breadthFirst 139 algoritmo conversione dei caratteri 71
pacchetto di esempio bzip 363
di ricerca breadth-first 139, test del carattere 71
bzipper esempio 365
239 esempio di conteggio dei caratteri 98
Linguaggio di programmazione C++ xiv, esempio di chat 254
xv, 361

www.it-ebooks.info
INDICE 369

server di chat 253 sicuro 275 struttura di dati ciclica 337


Funzione CheckQuota 312, 313 sicurezza 256, 257, 272, 365 dipendenza ciclica dei test 314
client, e-mail 312 con variabili condivise 257
client, SMTP 312 concomitanti dati
esempio di orologio 220, 222 server orologio 219 razza 259, 267, 275
server di orologio, concorrente attraversamento delle directory 247 struttura, ciclico 337
219 server eco 222 struttura, ricorsiva 101, 102, 107
chiudere la funzione incorporata 226, 228, cache non bloccante 272 tipo, set di bit 77
251 web crawler 239 driver di database, MySQL 284
chiudere, canale 228, 251 confinamento, serie 262 pacchetto database/sql 211, 288
goroutine più vicine 238, 250 confinamento, variabile 261 funzione daysAgo 114
chiusura di un canale 225 coerenza, sequenziale 268, 269 deadbeef 55, 80
chiusura, lessicale 136 dichiarazione const 14, 75 stallo 233, 240, 265 dichiarazione
Funzione cmplx.Sqrt 61 costante 14, 75
codice falso booleano 63 func 3, 29, 119
formato 3, 6, 9, 48 generatore, iota xiii, 77 importazione 3, 28, 42, 284, 285, 362
punto, Unicode 67 time.Minute 76
metodo 40, 155
produzione 301 time.Second 164
pacchetto 2, 28, 41, 285
Esempio di punto colorato 161 vero booleano 63
tipi, non tipizzati 78 costanti, livello di pacchetto 28
esempio di virgola 73 comando, ambito 45, 137
verifica di un 308 precisione di 78 vincoli,
costruzione 296 ombra 46, 49, 206, 212
argomento della riga di comando 4, 18,
contesa, blocco 267, 272 variabile breve 5, 7, 30, 31
33, dichiarazione, variabile breve 7
43, 179, 180, 290, 313 commutazione di contesto 280
struct 99
istruzione continue 24, 46 istruzione
commento tipo 39
continue, etichettata 249 contratti,
/*...*/ 5, 25 interfacce come 171 flusso di var 5, 30 dichiarazioni,
// 5, 25 controllo 46 ordine di 48
doc 42, 296 conversione esempio di decodifica, API di decodifica S-
// Uscita 326 canale da bidirezionale a expression 347, basata su token 213, 215,
commenti, +costruire 296 processi unidirezionale 231 347
sequenziali comunicanti da fetta di byte a stringa decodifica, espressione S 344
(CSP) xiii, 217 73 carattere 71 decodifica, XML 213
comunicazione, canale 225, 245 implicito 79 dichiarazione di decremento -- 5, 37
comparabilità 9, 38, 40, 53, 86, 93, restringimento 40, 55 esempio di dedup 97
97, 104 numerico 79 equivalenza profonda 87, 317, 358
matrice di operazione 40, 55, 64, 71, 78, 79, caso predefinito in select 246 caso
confronto 173, 187, 194, 208, 231, 353, 358 predefinito in switch 23 caso
83 predefinito in type switch 212
da runa a stringa 71 da
canale 225 rinvio della funzione anonima 146
runa a stringa 71 stringa
rinvio dell'esempio 150, 151
funzione 133 71
dichiarazione defer 144, 150, 264
interfaccia 184 da stringa a fetta di byte 40, 73
chiamata di funzione differita 144
mappa 96 da stringa a runa 71, 88 cancellazione della funzione
operatore == 40, 63 unsafe.Pointer 356 incorporata 94 algoritmo di ricerca
operatori 40, 93 funzione incorporata di copia 89 depth-first 136 dereferenziazione,
operatori, tabella di 53 esempio di conto alla rovescia 244, 245, implicita 159 diagramma
fetta 87 246 helloworld sottostringa 69
stringa 65 semaforo di conteggio 241 conduttura 228
struttura 104 copertura, dichiarazione 318, 320 fetta di crescita della
compilazione, separato 284 operatore di copertura, test 318 capacità 90 fetta di mesi 84
complemento ^, bitwise 53 funzione condivisione delle stringhe
incorporata complessa 61 tipo esempio di copertura_test 319 65
complesso 61 Profilo CPU 324 foro strutturale 355
letterale composito 14 esempio di crawl 240, 242, 243 sequenza di miniature 238
crawler, web concurrent 239 esempio di artefatto digitale
tipo composito xv, 14, 81
crawler, web 119 178 Dijkstra, Edsger 318
composizione, parallelo 224 sezione critica 263, 270, 275
composizione, tipo xv, 107, 162, 189 Dilbert 100
compilazione incrociata 295 grafo aciclico diretto 136, 284
pacchetto compress/bzip2 361 crittografia 55, 83, 121, 325
compressione 361 integrità attraversamento di directory,
concettuale xiv pacchetto crypto/sha256 83 concorrente 247
tipo di calcestruzzo 24, 171, 211, 214 esempio customSort 190
concomitanza 17, 217, 257
eccessivo 241, 242

www.it-ebooks.info
370 INDICE

sindacato discriminato 211, 213, 214 pacchetto errori 196 non vuoto 92
Funzione di visualizzazione 333 errori.Nuova funzione 196 escape schema 123, 133
esempio di visualizzazione 333 esadecimale 66 pacchetto, banca 258, 261, 263
funzione di visualizzazione 334 HTML 116 pacchetto, bzip 363
metodi di visualizzazione di tipo 351 ottale 66 pacchetto, formato 332
Funzione di distanza 156 sequenza 10 sequenze, pacchetto, geometria 156
commento 42, 296 tabella di 66 Unicode 68, pacchetto, http 192
107
doc.go file di commento 42, 296 pacchetto, link 138
URL 111
documentazione, pacchetto 296 pacchetto, promemoria 273
nome di dominio, percorso di escape delle variabili 36
pacchetto, parametri 348
importazione 284 punto . nel esempio di valutazione 198
pacchetto, stoccaggio 312, 313
modello 113 download di pacchetti multiplazione di eventi 244
292 pacchetto, tempconv 42
eventi 227, 244
Il dottor Stranamore 336 Esempio di funzione 302, 326
pacchetto, miniatura 235
drenaggio di un canale 229, 252 esempio palindromo 303, 305, 308
esempio 247, 249, 250 autoescape 117 params 348
duplice esempio 9, 11, 12 nome base 72 Parse 152
soppressione dei duplicati 276 bollitura 29 conduttura 228, 230, 231
dispatch dinamico 183 tipo Contatore di byte 173 playlist 187
dinamico, interfaccia 181 bzipper 365 rev. 86
cfr. 43 riverbero 223, 224
esempio 5, 7, 34, 309 numero di caratteri 98 server 19, 21
test dell'eco 309 chat 254 sexpr 340
server eco, concorrente 222 orologio 220, 222 Decodifica dell'espressione S 347
echo_test.go 310 Punto colorato 161 sha256 83
test efficaci, scrittura 316, 317 virgola 73 sonno 179
client di posta elettronica 312 conto alla rovescia 244, 245, 246 filatore 218
imbarazzantemente parallelo 235 campo prova_di_copertura 319 piazze 135
struct incorporato 161 incorporazione, striscia 240, 242, 243 somma 142
interfaccia 174 superficie 59, 203
customSort 190
incorporazione, struttura 104, 161 tempconv 39, 180, 289
dedup 97
Dipendente struct 100 conversione della temperatura 29
vuoto rinviare 150, 151
artefatto digitale 178 tempflag 181 test
tipo di interfaccia 176 di parola 303
istruzione select 245 display 333
miniatura 236, 237, 238
stringa 5, 7, 30 du 247, 249, 250
titolo 153
struttura 102 dup 9, 11, 12
topoSort 136
incapsulamento 168, 284 eco 5, 7, 34, 309
traccia 146
codifica API 213, 340 eval 198
Albero 102
codifica, espressione S 338 recuperare 16, 148
valori url 160
pacchetto codifica/json 107 fetchall 18
attendere 130
pacchetto codifica/xml 107, 213 fine findlinks 122, 125, 139
parola 303, 305, 308
del file (EOF) 131 ftoc 29
xmlselect 215
variabile d'ambiente github 110, 111
enum 77 appendInt 88
grafico 99
GOARCH 292, 295 eccezione 128, 149
helloworld 1 , 2
GOMAXPROCS 281, 321 eccessiva concorrenza 241, 242
http 192, 194, 195
GOOS 292, 295 esclusione, reciproca 262, 267
intset 166
GOPATH xvi, 291, 295 blocco esclusivo 263, 266, 270
questioni 112 operatore OR esclusivo ^ 53
GOROOT 292 questionihtml 115 back-off esponenziale 130
funzione uguale 87, 96 rapporto sulle questioni 114 esportazione del campo struct 101, 106, 109,
uguaglianza, puntatore 32 jpeg 287 110, 168
equivalenza, profondità 87, 317, 358 lissajous 14, 22, 35 file export_test.go 315
errore interfaccia integrata 196 mandelbrot 62 Metodo Expr.Check 202 espressione
tipo di errore incorporato 11, 128, 149, memo 275, 276, 277, 278, 279 indirizzabile 159, 341
196 metodi 351 valutatore 197
errore API 127, 152 film 108, 110 metodo 164
metodo error.Error 196 netcat 221, 223, 227 ricevere 225
funzione errorf 143 reteflag 78 Metodo Expr.Eval 199
strategie di gestione degli errori 128, 152,
310, 316

www.it-ebooks.info
INDICE 371

estensione di una fetta 86 numero in virgola flag.String 34


Linguaggio di marcatura estensibile mobile 56 fmt.Errorf 129, 196
(XML) 107 precisione 56, 57, 63, 78 fmt.Fprintf 172
pacchetto di prova esterno 285, 314 troncamento 40, 55 fmt.Printf 10
pacchetto fmt 2 fmt.Println 2
Messaggio di errore funzione fmt.Errorf 129, 196 fmt.Scanf 75
Fahrenheit tipo 39, test 306 funzione fmt.Fprintf 172 fmt.Sscanf 180
dichiarazione fallthrough 23, 212 funzione fmt.Printf 10 funzione
costante booleana false 63 forEachNode 133
fmt.Println 2 funzione fmt.Scanf
esempio di fetch 16, 148 formatoAtomo 332
75
esempio di fetchall 18 funzione fmt.Sscanf 180 gcd 37
funzione fib 37, 218 interfaccia fmt.Stringer 180, 210 gestore 19, 21, 152, 191, 194, 195,
Algoritmo di Fibonacci 37, 218 per l'ambito 47 348
campo
per la dichiarazione 6 html.Parse 121, 125
struttura anonima 104, 105, 106, http.DefaultServeMux 195
funzione forEachNode 133
162 http.Error 193
interfaccia funzioni esterne (FFI) 361
struttura incorporata 161 formato, codice 3, 6, 9, 48 http.Get 16, 18
esportazione delle strutture 101, 106, pacchetto di esempio di formato 332 http.Handle 195
109, funzione formatAtom 332 http.HandleFunc 19, 22, 195
110, 168 framework, web 193 http.ListenAndServe 19, 191
ordine, struttura 101, 355 esempio ftoc 29 http.NewRequest 253
selettore 156 dichiarazione func 3, 29, 119 funzione http.ServeMux 193
struttura 15, 99 anonimo 22, 135, 236 ipotesi 120
tag, omitempty 109 append incorporato 88, 90, 91 immag incorporato 61
tag, struct 109, 348 figura argomento 119 image.Decode 288
Lissajous 13 affermazione 316 image.RegisterFormat 288
Mandelbrot 63 Parametro di riferimento 302, 321 incr 33
Superficie 3D 58, 203 corpo, mancante 121 init 44, 49
Protocollo di trasferimento file (FTP) larghezzaPrima 139 intsToString 74
222 file
btoi 64 io.Copy 17, 18
blocco 46
bufio.NewReader 98 ioutil.ReadAll 16, 272
export_test.go 315
bufio.NewScanner 9 ioutil.ReadDir 247
nome, Microsoft Windows 72 nome,
POSIX 72 bufio.ScanWords 99 ioutil.ReadFile 12, 145
_test.go 285, 302, 303 bytes.Equal 86 io.WriteString 209
esempio di findlink 122, 125, 139 chiamata, differita 144 itob 64
pila a dimensione fissa 124 chiamata, valore ok da 128 json.Marshal 108
pacchetto bandiera 33, tappo incorporato 84, 232 json.MarshalIndent 108
179 bandiera CelsiusFlag 181 json.NewDecoder 111
go tool -bench 321 CheckQuota 312, 313 json.NewEncoder 111
go tool -benchmem 322 chiudere l'incasso 226, 228, 251 json.Unmarshal 110, 114
go tool -covermode 319 cmplx.Sqrt 61 len incorporato 4, 54, 64, 65, 81, 84,
go tool -coverprofile 319 go confronto 133 233
tool -cpuprofile 324 go tool complesso incorporato 61 links.Extract 138
-nodecount 325 copia incorporata 89 letterale 22, 135, 227
go tool -text 325 go
giorniAgo 114 log.Fatalf 49, 130
tool -web 326 godoc -
analysis 176 go list - eliminare l'incorporato 94 principale 2, 310
f 315 Display 333 fare incorporare 9, 18, 88, 94, 225
vai -gara 271 display 334 math.Hypot 156
go test -race 274 Distanza 156 math.Inf 57
go test -run 305 pari 87, 96 math.IsInf 57
go test -v 304 erroref 143 math.IsNaN 57
funzione flag.Args 34 errori.Nuovo 196 math.NaN 57
funzione flag.Bool 34 Esempio 302, 326 multivariato 11, 30, 37, 96, 125,
funzione flag.Duration 179 fib 37, 218 126
funzione flag.Parse 34 flag.Args 34 mustCopy 221
funzione flag.String 34 flag.Bool 34 net.Dial 220
flag.Valore interfaccia 179, 180 flag.Duration 179 net.Listen 220
flag.Parse 34 nuovo built-in 34
zero 132
os.Close 11

www.it-ebooks.info
372 INDICE

os.Exit 16, 34, 48 titolo 144, 145 strumento goimports 3, 44, 286
os.Getwd 48 tipo 119, 120 installare 295
os.IsExist 207 unicode.IsDigit 71 golang.org/x/net/html pacchetto
os.IsNotExist 207 unicode.IsLetter 71 122
os.IsPermission 207 unicode.IsLower 71 strumento golint 292
os.Open 11 unicode.IsSpace 93 elenco di partenza 298, 315
os.Stat 247 unicode.IsUpper 71 go list -f flag 315
panico incorporato 148, 149 unsafe.AlignOf 355 Variabile d'ambiente GOMAXPROCS 281,
321
parametro 119 non sicuro.Offsetdi 355
Variabile d'ambiente GOOS 292, 295
params.Unpack 349 non sicuro.Dimensione di 354
Variabile d'ambiente GOPATH xvi, 291,
png.Encode 62 url.QueryEscape 111 295
PopCount 45 utf8.DecodeRuneInString 69 repository gopl.io xvi
reale incorporato 61 utf8.RuneCountInString 69 vai -bandiera di gara 271
recuperare il 152 incorporato valore 132 variabile d'ambiente GOROOT 292
ricorsivo anonimo 137 variadico 142, 172 goroutine 18, 217, 233, 235
reflect.TypeOf 330 visita 122 più vicino 238, 250
reflect.ValueOf 331, 337 WaitForServer 130 identità 282
riflettere.Zero 345 walkDir 247 perdite 233, 236, 246
regexp.Compile 149 valore zero 132 monitor 261, 277
regexp.MustCompile 149 multiplexing 281 vs.
elenco dei risultati 119 garbage collection xi, xiii, 7, 35, 230, thread OS 280
runtime.Stack 151 353, 357 vai a correre 2, 294
RicercaSignificati 111 garbage collector, spostamento 357 go test 301, 302, 304 go
algoritmo GCD 37 test - flag race 274 go
sexpr.Marshal 340
Funzione gcd 37 test - flag run 305 go
sexpr.readList 347 test - flag v 304 goto
sexpr.Unmarshal 347 pacchetto di esempi di geometria 156 statement 24
metodo geometry.Point.Distance 156
firma 120 esempio di grafico 99
sort.Float64s 191 metodo getter 169 GraphViz 326 Griesemer,
Animazione GIF 13 GitHub
sort.Ints 191 Robert xi
issue tracker 110 github
sort.IntsAreSorted 191 crescita, pila 124, 280, 358
esempio 110, 111 Go
sort.Reverse 189 Parco giochi xvi, 326 protezione dei mutex 263
sort.Strings 95, 137, 191 Blog xvi, 326
Sprint 330 numero 110, 112, 358 intervallo di semiapertura 4
sqlQuote 211, 212 strumento go 2, 42, 44, 290 funzione di gestione 19, 21, 152, 191,
strconv.Atoi 22, 75 go tool -bench flag 321
194, 195, 348
strconv.FormatInt 75 go tool -benchmem flag 322
''accade prima'' relazione 226, 257,
strconv.Itoa 75 go tool -covermode flag 319
261, 277
strconv.ParseInt 75 go tool -coverprofile flag 319 go
''ha una'' relazione 162 tabella
strconv.ParseUint 75 tool -cpuprofile flag 324 go tool -
hash 9, 93
nodecount flag 325
Linguaggio di programmazione Haskell xiv
stringhe.Contiene 69
heap
strings.HasPrefix 69 go tool pprof 325
assegnazione 36
strings.HasSuffix 69 go tool - flag di testo 325
go tool - flag web 326 go profilo 324
stringhe.Indice 289 variabile 36
tool cover 318, 319 go doc
stringhe.Join 7, 12 esempio helloworld 1, 2 diagramma
tool 25
stringhe.Mappa 133 dichiarazione go 18, 218 di sottostringa helloworld 69 escape
strings.NewReader 289 Variabile d'ambiente GOARCH 292, 295 esadecimale 66
strings.NewReplacer 289 andare a costruire 2, 286, 293, 294 letterale esadecimale 55
stringhe.Split 12 vai doc 296 puntatore nascosto 357
strings.ToLower 72 Hoare, Tony xiii hole,
flag godoc -analisi 176 struct 354 HTML
strings.ToUpper 72 strumento godoc xvi, 25, 297, 326
template.Must 114
elemento di ancoraggio 122
vai a env 292 fuga 116
template.New 114 strumento gofmt 3, 4, 44, 286
Test 302
attacco di iniezione 115
andare a prendere xvi, 2, 292, 293 metacarattere 116
tempo.Dopo 245 vai ad aiutare 290
time.AfterFunc 164 parser 121
time.Now 220
time.Parse 220
tempo.Dal 114
time.Tick 244, 246

www.it-ebooks.info
INDICE 373

funzione html.Parse 121, 125 285, 362 interfacce come contratti 171
pacchetto html/template 113, 115 HTTP Importazione pacchetto interno 298
Richiesta GET 21, 127, 272, 348 vuoto 287 esempio intset 166
Richiesta POST 348 percorso 284 funzione intsToString 74
richiesta, annullamento di 253 percorso nome di dominio invarianti 159, 169, 170, 265, 284,
multiplexer di richiesta 193 284 ridenominazione 286 311, 352
esempio http 192, 194, 195 funzione incr 33 pacchetto io 174
pacchetto http di esempio 192 dichiarazione di incremento ++ 5, 37, interfaccia io.Closer 174
(*http.Client).Do metodo 253 94 operazione di indice, stringa 64
operatore di indirezione * 24, 32 funzione io.Copy 17, 18
variabile http.DefaultClient 253 io.Discard flusso 22
funzione http.DefaultServeMux 195 ciclo infinito 6, 120, 228
occultamento delle informazioni 168, variabile io.Discard 18
Funzione http.Error 193
284 variabile io.EOF 132
Funzione http.Get 16, 18
funzione init 44, 49 pacchetto io/ioutil 16, 145
Funzione http.Handle 195
inizializzazione interfaccia io.Reader 174
funzione http.HandleFunc 19, 22,
pigro 268 generatore di iota costante xiii, 77
195
pacchetto 44 funzione ioutil.ReadAll 16, 272
interfaccia http.Handler 191, 193
dichiarazione in if 22, Funzione ioutil.ReadDir 247
tipo http.HandlerFunc 194, 203 206 dichiarazione in funzione ioutil.ReadFile 12, 145
Funzione http.ListenAndServe 19, switch 24 io.Writer interfaccia 15, 22, 172,
191 elenco di inizializzatori 30 174, 186, 208, 209, 309
funzione http.NewRequest 253 attacco di iniezione, HTML 115 attacco io.WriteString funzione 209
http.Tipo di richiesta 21, 253 di iniezione, SQL 211
relazione "è un" 162, 175
(*http.Request).ParseForm tecniche di slice in-place 91
algoritmo di ordinamento a issue, Go 110, 112, 358 issue
metodo 22, 348 tracker, GitHub 110 esempio di
tipo http.ResponseWriter 19, 22, inserzione 101 tipo int 52
issue 112
191, 193 intero
esempio di issueshtml 115
funzione http.ServeMux 193 letterale 55
esempio di issuesreport 114 ordine
trabocco 53, 113 di iterazione, mappa 95 variabile di
funzione ipot 120
firmato 52, 54 iterazione, cattura 140 funzione itob
identificatore _, vuoto 7, 38, 95, 120, 126, senza segno 52, 54 64
test di integrazione 314
287 interfaccia Linguaggio di programmazione Java xv
identificatore, qualificato 41, 43 cedibilità 175 Notazione a oggetti JavaScript (JSON)
identità, goroutine 282 confronto 184 107, 338
Standard IEEE 754 56, 57 tipo dinamico 181 Linguaggio di programmazione
if, istruzione di inizializzazione in 22, JavaScript xv, 107
incorporazione 174
206 esempio jpeg 287 JSON
errore incorporato 196
ambito if-else 47 interfaccia 110
istruzione if-else 9, 22, 47 flag.Value 179, 180
interfaccia, Open Movie Database 113
immagina funzione incorporata fmt.Stringer 180, 210
interfaccia, xkcd 113
61 manipolazione delle immagini http.Handler 191, 193
smistamento 108
121 io.Closer 174
pacchetto immagine 62, 287 smascheramento 110
io.Reader 174
pacchetto immagine/colore 14 tipo json.Decoder 111
io.Writer 15, 22, 172, 174, 186,
funzione image.Decode 288 tipo json.Encoder 111
208, 209, 309
pacchetto image/png 288 funzione json.Marshal 108
JSON 110
funzione image.RegisterFormat 288 funzione json.MarshalIndent 108
chiamata al metodo 182
letterale immaginario 61 funzione json.NewDecoder 111
zero 182
immutabilità 261 funzione json.NewEncoder 111
trabocchetto 184
immutabilità, stringa 65, 73 funzione json.Unmarshal 110, 114
ReadWriteCloser 174
implementazione con slice, stack 92,
ReadWriter 174
215 parola chiave, tipo 212 parole
soddisfazione 171, 175 chiave, tabella di 27 Knuth,
implicito
sort.Interface 186 Donald 323
& 158, 167
tipo 171, 174
incarico 38 interfaccia{} tipo 143, 176, 331
conversione 79 ambito dell'etichetta 46
interfaccia etichetta, dichiarazione 46
dereferenza 159 asserzione del tipo 208, 210 etichettato
dichiarazione di importazione 3, 28, 42, tipo, vuoto 176
284, valore 181
con puntatore nullo 184
valore nullo 182

www.it-ebooks.info
374 INDICE

dichiarazione di interruzione 249 principale, pacchetto 2, 285, 310 getter 169


continua dichiarazione 249 rendere la funzione incorporata 9, 18, 88, (*http.Client).Do 253
dichiarazione 46 94, (*http.Request).ParseForm 22,
layout, memoria 354, 355 225 348
inizializzazione pigra 268 canale 18, 225 nome 156
leak, goroutine 233, 236, 246 creare una mappa 9, 18, 94 net.Conn.Close 220
operatore di spostamento a sinistra fare la fetta 88, 322 net.Listener.Accept 220
<< 54 Figura di Mandelbrot 63 (*os.File).Write 183
len funzione integrata 4, 54, 64, 65,
Insieme di Mandelbrot 61 percorso.Distanza 157
81, 84, 233 esempio di mandelbrot 62 promozione 161
blocco lessicale 46, 120, 135, 141, 212 mappa nome del ricevitore 157
chiusura lessicale 136 come set 96, 202 parametro del ricevitore 156
lifetime, variabile 35, 46, 135 confronto 96
pacchetto di esempio links 138 ricevitore tipo 157
elemento, inesistente 94, 95 reflect.Type.Field 348
funzione links.Extract 138
linguaggio di programmazione Lisp ordine di iterazione 95 reflect.Value.Addr 342
338 algoritmo di Lissajous 15 letterale 94 reflect.Value.CanAddr 342
Figura 13 di Lissajous lookup m[chiave] 94 lookup, reflect.Value.Interface 331,
ok valore da 96 make 9, 18,
esempio lissajous 14, 22, 35 342
94
elenco, inizializzatore 30 reflect.Value.Kind 332
letterale zero 95
gamma oltre il 94
selettore 156
array 82, 84 setter 169
composito 14 tipo 9, 93
con chiave slice 97 Stringa 40, 166, 329
funzione 22, 135, 227 (*sync.Mutex).Lock 21, 146, 263
valore zero 95
esadecimale 55 marshalling JSON 108 (*sync.Mutex).Unlock 21, 146,
immaginario 61 pacchetto matematica 14, 56 263
intero 55 matematica/grande pacchetto 63 (*sync.Once).Do 270
mappa 94 pacchetto math/cmplx 61 (*sync.RWMutex).RLock 266
ottale 55 funzione math.Hypot 156 (*sync.RWMutex).RUnlock 266
stringa grezza 66 Funzione math.Inf 57 (*sync.WaitGroup).Add 238
runa 56 (*sync.WaitGroup).Done 238
Funzione math.IsInf 57
fetta 38, 86 Funzione math.IsNaN 57 template.Funcs 114
stringa 65 Funzione math.NaN 57 template.Parse 114
struct 15, 102, 106 (*testing.T).Errorf 200, 304,
locale pacchetto math/rand 285, 308
esempio 275, 276, 277, 278, 306
blocco 46
279 (*testing.T).Fatale 306
variabile 29, 141
pacchetto di esempio memo 273 time.Time.Format 220
variabile, indirizzo di 32, 36
memoization 272 valore 164
ambito variabile 135 allocazione della memoria 36, 71, 89, (*xml.Decoder).Token 213
localizzazione dei pacchetti
169, esempio di metodi 351
291 serratura
contesa 267, 272 209, 322 metodi di un tipo, visualizzazione 351
layout di memoria 354, 355 nome di file di Microsoft Windows 72
esclusivo 263, 266, 270
metacarattere, metodo HTML 116 corpo di funzione mancante 121
mutex 102, 263, 264, 324 m[chiave], ricerca di mappe 94
(*bufio.Reader).ReadRune 98
non rientrante 265 piattaforme mobili 121
(*bufio.Scanner).Err 97
lettori 266 Linguaggio di programmazione
(*bufio.Scanner).Scan 9
condiviso 266 Modula-2 xiii
(*bufio.Scanner).Split 99
scrittore 266 modularità 283
(*bytes.Buffer).Grow 169
pacchetto log 49, 130, 170 monitor 264, 275
(*bytes.Buffer).WriteByte 74
funzione log.Fatalf 49, 130 goroutine di monitoraggio 261, 277
lookup m[chiave], mappa 94 (*bytes.Buffer).WriteRune 74
esempio di film 108, 110
lookup, valore ok da mappa 96 (*bytes.Buffer).WriteString spostamento del garbage collector
loop 74 357 multimap 160, 193
infinito 6, 120, 228 chiamata, interfaccia 182 assegnazione di valori multipli 37
gamma 6, 9 concatenamento 114 multiplexer, richiesta HTTP 193
variabile, cattura 141, 236, 240 dichiarazione 40, 155 multiplexing, evento 244
ambito variabile 141, 236 error.Error 196 multiplexing, goroutine 281 multithreading,
mentre 6 memoria condivisa
Controllo dell'applicazione 202
217, 257
espressione 164
funzione principale 2, 310 Expr.Eval 199
funzione multivariata 11, 30, 37, 96,
geometria.Punto.Distanza 156

www.it-ebooks.info
INDICE 375

125, 126 numero valore zero 5, 30 Thread OS vs. goroutine 280


funzione mustCopy 221 numerico pacchetto os 4, 206
mutex 145, 163, 256, 269 conversione 79 variabile os.Args 4
guardia 263 precisione 55, 78 funzione os.Close 11
serratura 102, 263, 264, 324 tipo 51 funzione os.Exit 16, 34, 48
lettura/scrittura 266, 267 *os.Tipo di file 11, 13, 172, 175, 185,
mutua esclusione 262, 267 Driver di Linguaggio di programmazione Oberon 336
database MySQL 284 xiii oggetto 156
os.FileInfo tipo 247
programmazione orientata agli
oggetti (OOP) 155, 168 (*os.File).Write metodo 183
nome funzione os.Getwd 48
fuga ottale 66
metodo 156 funzione os.IsExist 207
letterale ottale 55
metodo ricevitore 157 funzione os.IsNotExist 207
valore ok 37
pacchetto 28, 43 valore ok dalla ricezione del canale 229 funzione os.IsPermission 207
parametro 120 valore ok dalla chiamata di funzione os.LinkError tipo 207
spazio 41, 156, 283 128 valore ok dalla ricerca della mappa funzione os.Open 11
denominato 96 os.PathError tipo 207
risultato 120, 126 valore ok dall'asserzione del tipo 206 funzione os.Stat 247
risultato valore zero 120, 127 omitempty campo tag 109 Open esempio di schema 123, 133
tipo 24, 39, 40, 105, 157 Movie Database JSON // Commento di uscita 326 overflow,
convenzione di denominazione 28, 169, interfaccia 113 interi 53, 113
174, operazione, atomica 264 overflow, stack 124
289 operazione, conversione 40, 55, 64, 71,
denominazione, pacchetto 289 78, 79, 173, 187, 194, 208, 231, dichiarazione del pacchetto 2, 28, 41, 285
NaN (non è un numero) 57, 93 353, 358 pacchetto
conversione restrittiva 40, 55 operatore operatore API 284, 296, 311, 333, 352
di negazione ! 63 +=, -=, ecc., assegnazione 5 back-door 315
pacchetto netto 219 &, indirizzo di 24, 32, 94, 158, 167 esempio di banca 258, 261, 263
esempio di netcat 221, 223, 227 &^, AND-NOT 53 blocco 46
tipo net.Conn 220 &^, bit-clear 53 bufio 9
metodo net.Conn.Close 220 ^, complemento bitwise 53 byte 71, 73
funzione net.Dial 220 |, OR bitwise 166, 167 esempio di bzip 363
esempio di netflag 78 ==, confronto 40, 63 comprimere/bzip2 361
pacchetto net/http 16, 191 ^, OR esclusivo 53 crypto/sha256 83
funzione net.Listen 220 *, indirezione 24, 32 database/sql 211, 288
tipo net.Listener 220 <<, spostamento a sinistra 54 documentazione 296
metodo net.Listener.Accept 220 !, negazione 63 codifica/json 107
pacchetto net/smtp 312 %, resto 52, 166 codifica/xml 107, 213
pacchetto net/url 160 >>, spostamento a destra 54 errori 196
rete 121, 219 nuova &&, cortocircuito AND 63 test esterno 285, 314
funzione incorporata 34
||, cortocircuito OR 63 bandiera 33, 179
nuova, ridefinizione 35
+, concatenazione di stringhe 5, 65 fmt 2
nullo
-, unario 53 esempio di formato 332
canale 246, 249
+, unario 53 esempio di geometria 156
funzione 132
associatività 52 golang.org/x/net/html 122
interfaccia 182
precedenza 52, 63 html/template 113, 115
mappa 95
s[i:j], fetta 84, 86 esempio http 192
puntatore 32
s[i:j], sottostringa 65, 86 immagine 62, 287
puntatore, interfaccia con 184
ricevitore 159, 185 operatori immagine/colore 14
fetta 87 non assegnazione 36, 52 immagine/png 288
bloccante confronto 40, 93 tabella inizializzazione 44
binaria 52 tabella bitwise
cache 275 interno 298
53 tabella di confronto 53
cache, concorrente 272 io 174
ottimizzazione 264, 321, 323
canale ricezione 246 ottimizzazione, prematura 324 io/ioutil 16, 145
selezionare 246 operatore OR ||, cortocircuito 63 esempio di link 138
esempio non vuoto 92 ordine delle dichiarazioni 48 log 49, 130, 170
elemento di mappa inesistente 94, 95 ordine, campo struct 101, 355 principale 2, 285, 310
blocco non rientrante 265 organizzazione, spazio di lavoro 291 matematica 14, 56
pacchetto non standard 121 matematica/big 63
numero, a virgola mobile 56 matematica/cmplx 61

www.it-ebooks.info
376 INDICE

matematica/rand 285, 308 parser, HTML 121 Stampa %#x 56


esempio di promemoria 273 Linguaggio di programmazione Pascal Stampa %x 10, 55, 83
nome 28, 43 xiii percorso, ... 292, 299 codice di produzione 301
denominazione 289 pacchetto percorso 72 profilo
netto 219 metodo path.Distance 157 blocco 324
net/http 16, 191 percorso/percorso del file pacchetto CPU 324
rete/smtp 312 72 cumulo 324
net/url 160 Pike, Rob xi, xiii, 67, 107 profilazione 324 linguaggio
esempio di conduttura 228, 230, 231 di programmazione
non standard 121 Alef xiii APL
os 4, 206 conduttura 227
xiii
esempio di params 348 diagramma della pipeline 228
C++ xiv, xv, 361
percorso 72 trabocchetto, interfaccia 184
C xii, xv, 1, 6, 52, 260, 361
percorso/percorso file 72 trabocchetto, portata 140 Haskell xiv
riflettere 330 piattaforme, mobile 121 Java xv
Playground, Go xvi, 326 esempio
regexp 149 JavaScript xv, 107
di playlist 187
tempo di esecuzione 151 Lisp 338
funzione png.Encode 62 Modula-2 xiii
tipo 95, 186, 189
puntatore 24, 32, 34 Oberon xiii
esempio di stoccaggio 312, 313 aliasing 33 Pascal xiii
strconv 22, 71, 75
argomento 33, 83 Python xv, 193
stringhe 7, 71, 72, 289 Rubino xv, 193
uguaglianza 32
sincronia 237, 263 Schema xiii
nascosto 357
syscall 196, 208 Squeak, Newsqueak xiii promozione,
zero 32
tempconv esempio 42 metodo 161
ricevitore 158, 167
test 285, 302 buffer di protocollo 107
alle strutture 100, 103 Linguaggio di programmazione Python
testo/scanner 344
valore zero 32 xv, 193
testo/tabwriter 188 canale di polling 246
testo/template 113, 300 polimorfismo, ad hoc 211
identificatore qualificato 41, 43
esempio di miniatura 235 polimorfismo, sottotipo 211
interrogazione dei pacchetti
tempo 18, 77, 183 Funzione PopCount 45 298 carattere di citazione,
unicode 71 Grafica di rete portatile (PNG) 62 ' 56
unicode/utf8 69 Nome file POSIX 72
non sicuro 354 Standard POSIX xi, 55, 72, 197 gara
dichiarazione a livello di pacchetto 28 precedenza, operatore 52, 63 precisione
condizione 21, 257, 258, 259
pacchetti a virgola mobile 56, 57, 63, 78
rilevatore 271, 274
edificio 293 numerico 55, 78
paradossale 267
scaricare 292 di costanti 78
test randomizzati 307
localizzazione 291 nomi predeclamati, tabella di 28
astrazione prematura 216, 316, 317 loop di gamma 6, 9
interrogazione 298 intervallo su canale 229
palindromo 191 ottimizzazione prematura 324
intervallo su mappa 94
Printf %% 10
esempio palindromo 303, 305, 308 intervallo su stringa 69, 88
panico 64, 152, 253 Verbi di stampa, tabella di 10 {{range}} template action 113 raw
funzione integrata panico 148, 149 Printf %b 10, 54, 75 string literal 66 reachability 36
razza paradossale 267 Stampa %c 10, 56 leggere, stantio 268
composizione parallela 224 Stampa %d 10, 55 blocco dei lettori 266
parallelo, imbarazzantemente 235 Printf %e 10, 57 mutex di lettura/scrittura 266, 267
parametro parallelismo 217 Stampa %f 10, 57 Interfaccia ReadWriteCloser 174
... 91, 142, 143, 172 Printf %g 10, 57 Interfaccia ReadWriter 174
funzione 119 Printf %[n] 56 funzione incorporata reale
metodo ricevitore 156 Stampa %o 10, 55 61 ricevere
Printf %q 10, 56, 97 <-ch, canale 18, 225, 232
nome 120
Printf %s 10 espressione 225
passaggio 120
Printf %*s 134 canale non bloccante 246
inutilizzato 120
Printf %T 10, 80, 83, 184, 331 valore ok del canale 229 tipo canale
esempio di params 348 di sola ricezione <-chan T
pacchetto di esempio params 348 Printf %t 10, 83
230
funzione params.Unpack 349 Stampa %#v 106, 207
ricevitore
parentesi 4, 6, 9, 52, 63, 119, 146, Printf %v 10, 11
158, 285, 335, 345 Printf % x 71
Esempio di parsimonia 152

www.it-ebooks.info
INDICE 377

nome, metodo 157 193 funzione sexpr.Marshal 340


nullo 159, 185 runa letterale 56 funzione sexpr.readList 347
parametro, metodo 156 tipo di runa 52, 67 sexpr.Unmarshal funzione 347
puntatore 158, 167 conversione da rune slice a stringa 71 SHA256 message digest 83 sha256
tipo, metodo 157 conversione da rune a stringa 71 esempio 83
recuperare la funzione incorporata 152 pacchetto runtime 151 dichiarazione di shadowing 46, 49, 206,
ricorsione 121, 124, 247, 333, 339, API di runtime 324 212
345, 359 schedulatore runtime 281 condiviso
ricorsivo funzione runtime.Stack 151 serratura 266
funzione anonima 137 variabili 257
struttura dati 101, 102, 107 soddisfazione, interfaccia 171, 175 variabili, concomitanza con 257
Scalable Vector Graphics (SVG) 58 multithreading in memoria condivisa 217,
tipo 48
scheduler, runtime 281 257
ridefinizione del nuovo
Linguaggio di programmazione Scheme operatore di spostamento <<,
riferimento 35
xiii ambito sinistra 54 operatore di
chiamata da 83
dichiarazione 45, 137 spostamento >>, destra 54 breve
identità 87
per 47 dichiarazione delle variabili 5, 7, 30, 31
tipo 9, 12, 93, 120
if-else 47 ambito della dichiarazione di variabile
riflettere il pacchetto 330 22, 48 dichiarazione di variabile 7
etichetta 46
riflessione 329, 352, 359 cortocircuito
variabile locale 135
tipo reflect.StructTag 348 Operatore AND && 63
variabile loop 141, 236
reflect.Type tipo 330 valutazione 63
trabocchetto 140
metodo reflect.Type.Field 348 Operatore OR || 63 firma,
dichiarazione di variabile breve 22, 48 funzione 120
funzione reflect.TypeOf 330
interruttore 47 intero firmato 52, 54
reflect.Value tipo 331, 342
algoritmo di ricerca, breadth-first 139,
reflect.Value valore zero 332 s[i:j], operatore slice 84, 86
239
reflect.Value.Addr metodo 342 s[i:j], operatore di sottostringa 65, 86
reflect.Value.CanAddr metodo algoritmo di ricerca, depth-first 136
dichiarazione semplice 6, 22
342 Funzione SearchIssues 111
Dimensione della tabella 354
metodo reflect.Value.Interface 331, selezionare il caso 245
esempio di sonno 179
342 selezionare, caso predefinito in 246
fetta 4
metodo reflect.Value.Kind 332 selezionare, non bloccante 246
argomento 86
funzione reflect.ValueOf 331, 337 istruzione select 244, 245
capacità 88, 89
funzione reflect.Zero 345 istruzione select{} 245
diagramma di crescita della capacità 90
pacchetto regexp 149 recupero selettivo 152 confronto 87
funzione regexp.Compile 149 selettore, campo 156 estensione di una
funzione regexp.MustCompile 149 selettore, metodo 156 chiave 86, mappa con
espressione regolare 66, 149, 305, 321 semaforo, binario 262 97 letterali 38, 86
relazione, "accade prima" 226, 257, semaforo, conteggio 241 fare 88, 322
261, 277 punto e virgola 3, 6 nullo 87
relazione, "ha una" 162 inviare ch<-, canale 18, 225, 232 del diagramma dei mesi 84
relazione, "è una" 162, 175 inviare la dichiarazione 225 operatore s[i:j] 84, 86
operatore di resto % 52, 166 tipo di canale solo per l'invio chan<- T algoritmo di rotazione 86
rinominare l'importazione 286 230 tecniche, in loco 91
rendez-vous 234 compilazione separata 284 diagramma tipo 84
carattere sostitutivo d, Unicode 70, 98 di sequenza, miniatura 238 coerenza utilizzato come pila
repository, richiesta di sequenziale 268, 269 123 lunghezza zero
gopl.io xvi confinamento seriale 262 87
HTTP GET 21, 127, 272, 348 esempio di server 19, 21 valore zero 74, 87
HTTP POST 348 server Client SMTP 312 socket
multiplexer, HTTP 193 elenco chat 253 TCP 219
di risultati, funzione 119 orologio simultaneo 219 UDP 219
risultato, nome 120, 126 eco concomitante 222 Dominio Unix 219
ritorno, nudo 126 set, mappa come 96, 202 algoritmo di ordinamento, topologico 136
dichiarazione di restituzione 29, 120, metodo setter 169 pacchetto sort 95, 186, 189
125 esempio sexpr 340 funzione sort.Float64s 191
esempio rev. 86 Espressione S sort.Interfaccia interfaccia 186
esempio di riverbero 223, 224 decodifica esempio 347 funzione sort.Ints 191
operatore shift destro >> 54 decodifica 344 funzione sort.IntsAreSorted 191
Linguaggio di programmazione Ruby xv, codifica 338

www.it-ebooks.info
378 INDICE

tipo sort.IntSlice 191 funzione strconv.ParseInt 75 esempio di superficie 59, 203


funzione sort.Reverse 189 funzione strconv.ParseUint 75 figura di superficie, 3-D 58, 203
funzione sort.Strings 95, 137, 191 stream, io.Discard 22 SVG 58
esempio di spinner 218 Metodo stringa 40, 166, 329 stringa SWIG 361
Funzione Sprint 330 operatore di concatenazione + 5, 65 Coltello svizzero 290
API SQL 211 conversione 71 interruttore, caso predefinito in 23
Attacco SQL injection 211 immutabilità 65, 73 switch, dichiarazione di
funzione sqlQuote 211, 212 operazione di indice 64 inizializzazione in 24
esempio piazze 135 letterale 65 interruttore di portata 47
Squeak, linguaggio di programmazione letterale, grezzo 66 dichiarazione di commutazione 23, 47
Newsqueak xiii gamma oltre 69, 88 dichiarazione di commutazione, senza tag 24
pila diagramma di condivisione 65 dichiarazione di commutazione, tipo 210, 212,
assegnazione 36 test 71 214, 329
dimensione fissa 124 alla conversione della fetta di byte 40, interruttore, contesto 280
crescita 124, 280, 358 73 pacchetto di sincronizzazione 237, 263
implementazione con la fetta 92, 215 alla conversione della fetta di rune 71, canale sincrono 226
overflow 124 slice 88 tipo sync.Mutex 263, 269
utilizzata come 123 valore zero 5, 7, 30 (*sync.Mutex).Metodo di blocco 21,
traccia 149, 253 146, 263
confronto 65
variabile 36 (*sync.Mutex).Metodo di sblocco 21,
pacchetto stringhe 7, 71, 72, 289
dimensione variabile 124 146, 263
lettura stantia 268 funzione strings.Contains 69
funzione strings.HasPrefix 69 sync.Once tipo 270
standard
IEEE 754 56, 57 funzione strings.HasSuffix 69 (*sync.Once).Do metodo 270
POSIX xi, 55, 72, 197 funzione strings.Index 289 tipo sync.RWMutex 266, 270
funzione strings.Join 7, 12 (*sync.RWMutex).RLock metodo 266
Unicode 2, 27, 52, 66, 67, 69, 97
funzione strings.Map 133 (*sync.RWMutex).RUnlock metodo 266
dichiarazione tipo sync.WaitGroup 237, 250, 274
--, decremento 5, 37 funzione strings.NewReader 289
(*sync.WaitGroup).Add metodo 238
++, incremento 5, 37, 94 funzione strings.NewReplacer 289 (*sync.WaitGroup).Done metodo 238
assegnazione 5, 7, 36, 52, 94, 173 stringhe.Tipo di lettore 289
pacchetto syscall 196, 208
pausa 24, 46 stringhe.Tipo di sostituto 289 tipo syscall.Errno 196, 197 API delle
continuare 24, 46 funzione strings.Split 12 chiamate di sistema 196
copertura 318, 320 funzione strings.ToLower 72
rinviare 144, 150, 264 funzione strings.ToUpper 72 tabella di
caduta 23, 212 dichiarazione della operatori binari 52
struttura 99 struct
per 6 operatori bitwise 53
confronto 104
andare 18, 218 operatori di confronto 53
incorporazione 104, 161
vai al 24 sequenze di escape 66
Dipendente 100
if-else 9, 22, 47 parole chiave 27
vuoto 102 nomi predeterminati 28
etichetta 46
campo 15, 99 Verbi di stampa 10
etichettato 46
campo, anonimo 104, 105, 106,
ritorno 29, 120, 125 Codifiche UTF-8 67
162 tavolo, dimensione 354
select{} 245
campo, incorporato 161
selezionare 244, 245 test guidati da tabelle 200, 306, 319
campo, esportazione di 101, 106, 109, tag, campo struct 109, 348
inviare 225
dichiarazione di variabile breve 7 110, istruzione switch senza tag 24 tag,
semplice 6, 22 168 costruzione 296
interruttore 23, 47 ordine di campo 101, 355 Presa TCP 219
interruttore senza etichetta 24 etichetta di campo 109, 348 tecniche, slice in-place 91 tempconv
foro 354 esempio 39, 180, 289 tempconv esempio
interruttore di tipo 210, 212, 214, 329
diagramma dei fori 355 pacchetto 42 conversione di temperatura
irraggiungibile 120 esempio 29 tempflag esempio 181
esempio di confezione di stoccaggio letterale 15, 102, 106
modello API 115 modello
letterale, indirizzo di 103
312, 313
puntatore a 100, 103
Stranamore, Dr. 336 tipo 15, 24, 99
strategie, gestione degli errori 128, 152, struct{} tipo 227, 241, 250 struct
310, 316 tipo, senza nome 163 struct valore
pacchetto strconv 22, 71, 75 zero 102 sostituibilità 193
funzione strconv.Atoi 22, 75 operatore di sottostringa s[i:j] 65, 86
funzione strconv.FormatInt 75 polimorfismo del sottotipo 211
Funzione strconv.Itoa 75 esempio di somma 142

www.it-ebooks.info
INDICE 379

| in 113 decodificatore basato su token API 213, metodo ricevitore 157


azione, {{range}} 113 215, mancata corrispondenza 55
punti . in 113 347 nominati 24, 39, 40, 105, 157
metodo template.Funcs 114 strumento di decodifica XML basato su net.Conn 220
tipo template.HTML 116 token 213 net.Listener 220
funzione template.Must 114 cgo 361, 362 numerico 51
template.Nuova funzione 114 vai 2, 42, 44, 290 *os.File 11, 13, 172, 175, 185,
metodo template.Parse 114 vai doc 25 336
Funzione di test 302 godoc xvi, 25, 297, 326 os.FileInfo 247
test gofmt 3, 4, 44, 286 os.LinkError 207
black-box 310 goimports 3, 44, 286 os.PathError 207
fragile 317 golint 292 ricorsivo 48
carattere 71 algoritmo di ordinamento topologico riferimento 9, 12, 93, 120
copertura 318 136 reflect.StructTag 348
dipendenza, ciclica 314 esempio di topoSort 136 reflect.Type 330
eco 309 esempio di traccia 146 riflettere.Valore 331, 342
messaggio di errore 306 traccia, pila 149, 253 runa 52, 67
integrazione 314 albero, binario 102
di parola esempio 303 fetta 84
esempio di treesort 102 sort.IntSlice 191
pacchetto, esterno 285, 314
costante booleana vera 63 stringhe.Reader 289
stringa 71 troncamento, virgola mobile 40, 55
white-box 311 stringhe.Replacer 289
assegnazione di tuple 31, 37
affermazione 306 struct{} 227, 241, 250
dichiarazione del tipo 39
file _test.go 285, 302, 303 struttura 15, 24, 99
tipo Parola chiave 212
pacchetto di test 285, 302 test tipo interruttore, caso in 212
un comando 308 estratto 24, 171 interruttore, caso predefinito in 212
randomizzato 307 aggregato 81, 99 dichiarazione di commutazione 210, 212,
guidato da tavolo 200, 306, 319 array 81 214,
collaudo.B tipo 321 affermazione 205, 211 329
test.T tipo 302 asserzione, interfaccia 208, 210 sync.Mutex 263, 269
(*testing.T).Errorf metodo 200, asserzione, valore ok da 206 sync.Once 270
304, 306 bool 63 sync.RWMutex 266, 270
(*testing.T).Metodo fatale 306 bufio.Scanner 9 sync.WaitGroup 237, 250, 274
test, scrittura efficace 316, 317 byte 52 syscall.Errno 196, 197
pacchetto testo/scanner 344 bytes.Buffer 74, 169, 172, 185 template.HTML 116
pacchetto testo/tabwriter 188 Celsius 39 collaudo.B 321
pacchetto testo/template 113, 300 chan 225 test.T 302
Thompson, Ken xi, 67 canale 18 time.Duration 76, 179
filo 218, 280 <-chan T, canale di sola ricezione 230 time.Time 114
archiviazione thread-local 282 chan<- T, canale di solo invio 230 uint 52
Figura della superficie 3D 58, 203 complesso 61 uintptr 52, 354, 357
esempio di miniatura 236, 237, 238 composito xv, 14, 81 sottostante 39
esempio di miniatura pacchetto 235 composizione xv, 107, 162, 189 canale unidirezionale 230, 231
diagramma di sequenza miniatura 238 concreti 24, 171, 211, 214
pacchetto tempo 18, 77, 183 struttura senza nome 163
metodi di visualizzazione di
time.After funzione 245 unsafe.Pointer 356
un'interfaccia vuota 351 176
funzione time.AfterFunc 164 url.URL 193
errore incorporato 11, 128, 149, 196
tipo time.Duration 76, 179 Fahrenheit 39
tipi, costante non tipizzata 78
costante time.Minute 76 funzione 119, 120
funzione time.Now 220 Presa UDP 219
http.HandlerFunc 194, 203
funzione time.Parse 220 tipo uint 52
http.Request 21, 253
tipo uintptr 52, 354, 357
time.Second costante 164 http.ResponseWriter 19, 22, operatore unario + 53
tempo.Dalla funzione 114 191, 193 operatore unario - 53 canale
funzione time.Tick 244, 246 int 52 non bufferizzato 226
time.Time tipo 114 interfaccia{} 143, 176, 331 comportamento non definito 260
metodo time.Time.Format 220 interfaccia 171, 174 matrice sottostante 84, 88, 91, 187
esempio di titolo 153 interfaccia dinamica 181 tipo sottostante 39 Unicode
funzione titolo 144, 145 json.Decoder 111 punto di codice 67
json.Encoder 111 fuga 68, 107
mappa 9, 93 carattere sostitutivo d 70, 98

www.it-ebooks.info
380 INDICE

standard 2, 27, 52, 66, 67, 69, 97 variabili, condivise 257


pacchetto unicode 71 pila a dimensione variabile 124
funzione unicode.IsDigit 71 funzione variadica 142, 172
funzione unicode.IsLetter 71 vettore, bit 165
funzione unicode.IsLower 71 vendita 293
funzione unicode.IsSpace 93 visibilità 28, 29, 41, 168, 297
funzione unicode.IsUpper 71 funzione di visita 122
pacchetto unicode/utf8 69
tipo di canale unidirezionale 230, 231 esempio di attesa 130
sindacato, discriminato 211, 213, 214 Funzione WaitForServer 130
blocco universo 46 funzione walkDir 247 web
socket di dominio Unix 219 crawler 119
unmarshaling JSON 110 tipo crawler, concorrente 239
struct senza nome 163 framework 193
variabile senza nome 34, 88 while loop 6 test
dichiarazione irraggiungibile 120 white-box 311
pacchetto non sicuro 354 Wilkes, Maurice 301 Wirth,
funzione unsafe.AlignOf 355 Niklaus xiii
funzione unsafe.Offsetof 355 esempio di parola 303, 305, 308
esempio di parola, prova di 303
non sicuro.Conversione di un
organizzazione dello spazio di
puntatore 356 lavoro 291
unsafe.Pointer tipo 356 blocco dello scrittore 266
unsafe.Pointer valore zero 356
unsafe.Sizeof funzione 354 scrivere test efficaci 316, 317
intero senza segno 52, 54 tipi di
costanti non tipizzate 78 xkcd Interfaccia JSON 113
parametro non utilizzato 120 Decodifica XML 213
URL 123 XML (Extensible Markup Language) 107
(*xml.Decoder).Token metodo 213
Escape URL 111
esempio xmlselect 215
funzione url.QueryEscape 111
url.URL tipo 193
fetta di lunghezza zero 87
Esempio di valori url 160 valore zero
UTF-8 66, 67, 98 array 82
Codifiche UTF-8, tabella di 67 booleano 30
funzione utf8.DecodeRuneInString 69 canale 225, 246
funzione utf8.RuneCountInString 69
funzione 132
utf8.UTFMax valore 98
interfaccia 182
mappa 95
valore
risultato nominato 120, 127
indirizzabile 32
numero 5, 30
chiamata da 83, 120, 158
puntatore 32
funzione 132
reflect.Value 332
interfaccia 181
fetta 74, 87
metodo 164
stringa 5, 7, 30
utf8.UTFMax 98
dichiarazione var 5, 30 struttura 102
variabile unsafe.Pointer 356
confinamento 261
cumulo 36
http.DefaultClient 253
io.Discard 18
io.EOF 132
vita 35, 46, 135
locale 29, 141
os.Args 4
pila 36
senza nome 34, 88
variabili, escape 36

www.it-ebooks.info
UNISCITI AL
INFORMARE
TEAM DI AFFILIATI!

Amate i nostri titoli e vi piace


condividerli con i vostri colleghi e amici... perché
e non guadagnare qualche soldo nel f a r l o !

Se avete un sito web, un blog o anche una pagina


Facebook, potete iniziare a guadagnare inserendo i link
InformIT nella vostra pagina.

Ogni volta che un visitatore clicca su questi link e fa un


acquisto su informit.com, voi guadagnate commissioni*
su tutte le vendite!

Ogni vendita che porterete sul nostro sito vi farà


guadagnare una commissione. Tutto ciò che dovete fare
è pubblicare i link ai titoli che desiderate, quanti ne volete,
e noi ci occuperemo del resto.

CANDIDATEVI E INIZIATE A
LAVORARE!
È facile e veloce da applicare.
Per saperne di più visitate il
sito:
http://www.informit.com/affiliates/
*Valido per tutte le vendite di libri, eBook e video su www.informit.com

www.it-ebooks.info

Potrebbero piacerti anche