Go - from Google


Struct e metodi      

nota: questo capitolo è da intendersi come abbozzo,

Anche in Go è possibile definire dei tipi da parte dell'utente. Questa pratica è comunissima in chi usa linguaggi puri a oggetti. Go, come detto nell'introduzione non può dirsi esattamente un linguaggio object oriented ma ciò non toglie che qualche cosa che si rifà a quel paradigma di successo. Le struct sono entità in qualche modo, o forse meglio dire "in parte", simili alle classi di C++ o Java o altri linguagi a oggetti, almeno da un punto di vista morfologico e sono costituite da un raggruppamento di valori di vario tipo (che si chiamano campi) accessibili singolarmente. Le struct sono tipi valore e sono definiti internamente tramite la funzione new. Da un punto di vista pratica ogni struct è creato come blocco contiguo di memoria insieme ai suoi elementi costitutivi e questo è estremamente vantaggioso da un punto di vista prestazionale. Per quanto riguarda l'aspetto formale formale abbiamo:

type nomestruttura struct {
tipo1 campo1
tipo2 campo2
.....
}

Una sintassi alternativa è la seguente, ad esempio:

type nomestruttura { a, b int }

adatta per strutture di piccole dimensioni. Come al solito è possibile usare il nome _ (underscore) per un campo che non sarà mai usato. Va detto che i campi interni non sono obbligatori e potremmo pertanto avere una struct completamente vuota al suo interno. Vediamo un esempio


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

func main() {

type st01 struct {
x1 int
x2 int
}
var s1 st01
s1.x1 = 0
s1.x2 = 1
fmt.Println(s1.x1)
}

Nel 10.1 vediamo la definizione di una nuova struct alla riga 6 e fino alla 9, mentre alla 10 abbiamo un esempio di istanziazione. Alla 11 ed alla 12 viene mostrato come accedere a singoli elementi interni, come prevedibile tramite il . (punto).
Un altro sistema di inizializzazione fa usa della solita keyword new; ai fini pratici nulla sarebbe cambiato scrivendo la riga 10 come:

s1 := new(st01)

Ricordo ancora che le struct sono tipi valore per cui new in pratica restituisce un puntatore ad un valore.
Interessante, utile per gli esempi che seguiranno, l'output della scrittura diretta del contenuto di una struttura:

fmt.Printl(s1)

Vediamo ora un esempio che ci chiarirà un po' meglio alcuni concetti:

  Esempio 10.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import "fmt"

func main() {

type st01 struct {
x1 int
x2 int
}
var s01 st01
var s02 = s01
var s03 = &s01
s02.x1 = 5
fmt.Println(s01)
fmt.Println(s02)
fmt.Println(s03)
s03.x2 = 5
fmt.Println(s01)
fmt.Println(s02)
fmt.Println(s03)
}

Che presenta il seguente output:

{0 0}
{5 0}
&{0 0}
{0 5}
{5 0}
&{0 5}

la riga 11 definisce una copia della variabile s01 mentre la 12 un puntatore alla stessa. Le variazioni indotte su s02 sono limitate ad essa mentre quelle definite su s03 hanno effetto anche su s01, come del resto prevedibile. E' abbastanza importante ricordare questi aspetti che hanno ovviamente una valenza generale.

Come sempre Go dà una certa importanza alla differenza tra maiuscole e minuscole. In particolare per quanto riguarda le struct se vogliamo che i campi siano visibili dall'esterno del package al quale appartengono essi devono avere l'iniziale minuscola.

Interessante è anche la possibilità di definire all'interno di una struct dei campi anonimi. Essi sono dotati di tipo ma non del nome che li identifica. Un esempio didatticamente interessante può essere quello che prevede che all'interno di una struct ci sia un'altra struct anonima. La cosa presenta anche una piccola particolarità sintattica, come vediamo nel seguente esempio:

  Esempio 10.3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import "fmt"

func main() {

type st01 struct {
x1 int
x2 int
}
type st02 struct {
st01;
y1 int
y2 int
}
var s01 st01
var s02 st02
fmt.Println(s01)
fmt.Println(s02)
}

La struct02 prevede, alla riga 11, un campo anonimo di tipi st01, ovvero la struttura definita a partire dalla riga 6. Come si vede la riga 11 necessita di un separatore e questo è il ; (punto e virgola) che, appunto, deve essere aggiunto in coda ai campi anonimi. L'output è molto semplice, comprensibile e significativo:

{0 0}
{{0 0} 0 0}


L'inizializzazione potrebbe essere fatta con una riga come la prima delle due seguenti:

s := st02{st01{1,2},3,4}
fmt.Println(s.x2)

la seconda restituisce come output il numero 2. L'esempio è interessante ma ovviamente i campi anonimi possono essere di qualsiasi tipo:

type st01 struct {
int;
float;
st string
}

l'istanziazione è consueta:

s := s01(3, 3.0, "aa")

e poi:

fmt.Println(s.int, s.float, s.st)

Molto importante è annotare come nelle struct non si trovino le classiche funzioni che invece fanno parte delle classi (e anche delle strutture) di molti altri linguaggi. Go non ha questa caratteristica ma permette di "attaccare" un metodo a (quasi) qualunque tipo. Quanto diremo per le strutture ha quindi una valenza molto più generale. Tra l'altro vedremo come si farà un largo uso di puntatori, per questioni di performance. Si noti che usiamo proprio la parola metodo che, nel mondo di Go, è una funzione che agisce su una variabile di un certo tipo detta ricevente. Il ricevente stesso al quale un metodo afferisce deve essere dichiarato nel package di quel metodo stesso e in generale nello stesso package di tutti i suoi metodi. Inoltre il suo nome deve essere usato all'interno del metodo. In conseguenza di ciò, una struct può pertanto essere arricchita tramite una funzione. In questo è del tutto simile ad una classe anche se la funzione non è inclusa nel corpo della struct. Se abbiamo più di un metodo associato ad uno stesso tipo si parla di insieme di metodi. Non esiste overload dei metodi, quindi su un tipo esiste un solo metodo con un certo nome, tuttavia un metodo con lo stesso nome può essere definito su tipi diversi.
Per chiarire meglio la differenza tra funzioni e metodi valga questo chiaro esempio

funzione -> funzione(x)
metodo -> x.metodo() - si noti la parentesi tonda necessaria dopo il nome del metodo

ovvero la funzione ha la variabile come parametro mentre il metodo è chiamato sulla variabile.
Da un punto di vista sintattico una definizione di metodo è simile a quella valida per le funzioni. Come sostanziale differenza abbiamo che dopo keyword func ed il nome della variabile deve essere necessariamente specificato come variabile e tipo,

func (var tipo) nomefunc .....

Ma per chiarire un po' la cosa niente di meglio di un esempio:

  Esempio 10.4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import "fmt"

type st01 struct {
x1 int
x2 int
}

func (s *st01) valori() {
s.x1 = 2
s.x2 = 3
}

func main() {
var s1 st01
s1.valori()
fmt.Println(s1)
}

Alla riga 9 abbiamo la definizione del nostro metodo che, come si vede, riferisce ad un puntatore alla struttura definita alla riga 4. Alla riga 16 possiamo usare il metodo in maniera analoga a come lo avremmo usato in un linguaggio a oggetti puro, ovvero selezionandolo tramite il . (punto). A questo punto potreste essere interessati a vedere cosa succedere eliminando l'asterisco che definisce il puntatore. Se ci pensate bene è una istanziazione in piena regola e si tratta di un valore per cui viene creata una copia.... perciò..... attenzione.
Come abbiamo detto è possibile applicare un metodo anche a un altro tipo, non necessariamente ad una struct. Vediamo un esempio che usa un array, l'esempio è ispirato a quello presente nelle slide di presentazione di Go e mi è molto piaciuto perchè particolarmente semplice:

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

type Vint []int

func (v Vint) Sum() (s int) {
for _, x := range v {
s += x
}
return
}

func main() {
fmt.Println(Vint{1, 2, 3}.Sum())
}

In questo caso, riga 4, stiamo lavorando con un array di interi ed operiamo direttamente su di esso.