1 Concetti base di Struct

Nel linguaggio Go, uno struct è un tipo di dato composito utilizzato per aggregare tipi di dati diversi o identici in un'unica entità. Gli struct occupano una posizione significativa in Go in quanto costituiscono un aspetto fondamentale della programmazione orientata agli oggetti, sebbene con lievi differenze rispetto ai linguaggi di programmazione orientati agli oggetti tradizionali.

La necessità degli struct deriva dai seguenti aspetti:

  • Organizzazione delle variabili con forte correlazione per migliorare la manutenibilità del codice.
  • Fornire un modo per simulare "classi", facilitando funzionalità di incapsulamento e aggregazione.
  • Quando si interagisce con strutture dati come JSON, record di database, ecc., gli struct offrono uno strumento di mappatura conveniente.

L'organizzazione dei dati con gli struct consente una rappresentazione più chiara di modelli di oggetti del mondo reale come utenti, ordini, ecc.

2 Definizione di uno Struct

La sintassi per definire uno struct è la seguente:

type NomeStruct struct {
    Campo1 TipoCampo1
    Campo2 TipoCampo2
    // ... altre variabili di membro
}
  • La parola chiave type introduce la definizione dello struct.
  • NomeStruct è il nome del tipo struct, seguendo le convenzioni di denominazione di Go, di solito è scritto in maiuscolo per indicarne l'esportabilità.
  • La parola chiave struct indica che si tratta di un tipo struct.
  • All'interno delle parentesi graffe {}, vengono definite le variabili di membro (campi) dello struct, ciascuna seguita dal proprio tipo.

Il tipo dei membri dello struct può essere qualsiasi tipo, inclusi tipi di base (come int, string, ecc.) e tipi complessi (come array, slice, un altro struct, ecc.).

Ad esempio, definendo uno struct che rappresenta una persona:

type Persona struct {
    Nome   string
    Età    int
    Email  []string // può includere tipi complessi, come slice
}

Nel codice sopra, lo struct Persona ha tre variabili di membro: Nome di tipo stringa, Età di tipo intero e Email di tipo slice di stringhe, indicando che una persona può avere più indirizzi email.

3 Creazione e Inizializzazione di uno Struct

3.1 Creazione di un'istanza di Struct

Ci sono due modi per creare un'istanza di struct: dichiarazione diretta o utilizzo della parola chiave new.

Dichiarazione diretta:

var p Persona

Il codice sopra crea un'istanza p di tipo Persona, dove ciascuna variabile di membro dello struct ha come valore iniziale il valore zero del relativo tipo.

Utilizzo della parola chiave new:

p := new(Persona)

La creazione di un struct utilizzando la parola chiave new restituisce un puntatore allo struct. La variabile p in questo punto è di tipo *Persona, puntando a una variabile di tipo Persona appena allocata dove le variabili di membro sono state inizializzate ai valori zero.

3.2 Inizializzazione delle istanze di Struct

Le istanze di struct possono essere inizializzate in un'unica operazione durante la creazione, utilizzando due metodi: con nomi di campo o senza nomi di campo.

Inizializzazione con nomi di campo:

p := Persona{
    Nome:   "Alice",
    Età:    30,
    Email:  []string{"[email protected]", "[email protected]"},
}

Quando si inizializza con la forma di assegnazione dei campi, l'ordine dell'inizializzazione non deve coincidere con l'ordine della dichiarazione dello struct e i campi non inizializzati manterranno i loro valori zero.

Inizializzazione senza nomi di campo:

p := Persona{"Bob", 25, []string{"[email protected]"}}

Quando si inizializza senza nomi di campo, assicurarsi che i valori iniziali di ciascuna variabile di membro siano nello stesso ordine della definizione dello struct e nessun campo può essere omesso.

Inoltre, gli struct possono essere inizializzati con campi specifici e qualsiasi campo non specificato assumerà valori zero:

p := Persona{Nome: "Charlie"}

In questo esempio, solo il campo Nome è inizializzato, mentre Età e Email assumeranno entrambi i rispettivi valori zero.

4 Accesso ai membri dello Struct

Accedere alle variabili di membro di uno struct in Go è molto semplice, raggiungibile utilizzando l'operatore punto (.). Se si ha una variabile struct, è possibile leggere o modificare i valori dei suoi membri in questo modo.

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func main() {
    // Create a variable of type Person
    p := Person{"Alice", 30}

    // Access struct members
    fmt.Println("Nome:", p.Name)
    fmt.Println("Età:", p.Age)

    // Modify member values
    p.Name = "Bob"
    p.Age = 25

    // Access the modified member values again
    fmt.Println("\nNome Aggiornato:", p.Name)
    fmt.Println("Età Aggiornata:", p.Age)
}

In questo esempio, definiamo prima una struttura Person con due variabili membro, Name e Age. Creiamo quindi un'istanza di questa struttura e mostriamo come leggere e modificare questi membri.

5 Composizione e Incorporamento di Struct

Le struct non possono solo esistere indipendentemente, ma possono anche essere composte e annidate insieme per creare strutture dati più complesse.

5.1 Struct Anonime

Una struct anonima non dichiara esplicitamente un nuovo tipo, ma utilizza direttamente la definizione della struct. Questo è utile quando è necessario creare una struct una volta e usarla semplicemente, evitando la creazione di tipi non necessari.

package main

import "fmt"

func main() {
    // Definisci e inizializza una struct anonima
    person := struct {
        Nome string
        Età  int
    }{
        Nome: "Eve",
        Età:  40,
    }

    // Accedi ai membri della struct anonima
    fmt.Println("Nome:", person.Nome)
    fmt.Println("Età:", person.Età)
}

In questo esempio, invece di creare un nuovo tipo, definiamo direttamente una struct e ne creiamo un'istanza. Questo esempio illustra come inizializzare una struct anonima e accedere ai suoi membri.

5.2 Incorporamento di Struct

L'incorporamento di struct coinvolge l'annidare una struct come membro di un'altra struct. Ciò ci consente di costruire modelli di dati più complessi.

package main

import "fmt"

// Definisci la struct Address
type Address struct {
    Città    string
    Paese string
}

// Incorpora la struct Address nella struct Person
type Person struct {
    Nome    string
    Età     int
    Indirizzo Address
}

func main() {
    // Inizializza un'istanza di Person
    p := Person{
        Nome: "Charlie",
        Età:  28,
        Indirizzo: Address{
            Città:    "New York",
            Paese: "USA",
        },
    }

    // Accedi ai membri della struct incorporata
    fmt.Println("Nome:", p.Nome)
    fmt.Println("Età:", p.Età)
    // Accedi ai membri della struct Address
    fmt.Println("Città:", p.Indirizzo.Città)
    fmt.Println("Paese:", p.Indirizzo.Paese)
}

In questo esempio, definiamo una struct Address e la incorporiamo come membro nella struct Persona. Quando creiamo un'istanza di Persona, creiamo contemporaneamente anche un'istanza di Address. Possiamo accedere ai membri della struct incorporata usando la notazione punto.

6 Metodi delle Struct

Le funzionalità della programmazione orientata agli oggetti (OOP) possono essere implementate attraverso i metodi delle struct.

6.1 Concetti di Base dei Metodi

Nel linguaggio Go, anche se non esiste un concetto tradizionale di classi e oggetti, è possibile ottenere funzionalità simili all'OOP associando metodi alle struct. Un metodo di una struct è un tipo speciale di funzione che si associa a un tipo di struct specifico (o a un puntatore a una struct), consentendo a quel tipo di avere il proprio set di metodi.

// Definisci una semplice struct
type Rettangolo struct {
    lunghezza, larghezza float64
}

// Definisci un metodo per la struct Rectangle per calcolare l'area del rettangolo
func (r Rettangolo) Area() float64 {
    return r.lunghezza * r.larghezza
}

6.2 Ricevitori di Valore e Ricevitori di Puntatore

I metodi possono essere categorizzati come ricevitori di valore e ricevitori di puntatore in base al tipo di ricevitore. I ricevitori di valore utilizzano una copia dello struct per chiamare il metodo, mentre i ricevitori di puntatore utilizzano un puntatore allo struct e possono modificare lo struct originale.

// Definire un metodo con un ricevitore di valore
func (r Rettangolo) Perimetro() float64 {
    return 2 * (r.lunghezza + r.larghezza)
}

// Definire un metodo con un ricevitore di puntatore, che può modificare lo struct
func (r *Rettangolo) ImpostaLunghezza(nuovaLunghezza float64) {
    r.lunghezza = nuovaLunghezza // può modificare il valore originale dello struct
}

Nell'esempio sopra, Perimetro è un metodo con ricevitore di valore, chiamarlo non cambierà il valore di Rettangolo. Tuttavia, ImpostaLunghezza è un metodo con ricevitore di puntatore, e chiamare questo metodo influenzerà l'istanza originale di Rettangolo.

6.3 Invocazione dei Metodi

È possibile chiamare i metodi di uno struct utilizzando la variabile dello struct e il suo puntatore.

func main() {
    rett := Rettangolo{lunghezza: 10, larghezza: 5}

    // Chiamare il metodo con un ricevitore di valore
    fmt.Println("Area:", rett.Area())

    // Chiamare il metodo con un ricevitore di valore
    fmt.Println("Perimetro:", rett.Perimetro())

    // Chiamare il metodo con un ricevitore di puntatore
    rett.ImpostaLunghezza(20)

    // Chiamare nuovamente il metodo con un ricevitore di valore, nota che la lunghezza è stata modificata
    fmt.Println("Dopo la modifica, Area:", rett.Area())
}

Quando si chiama un metodo utilizzando un puntatore, Go gestisce automaticamente la conversione tra valori e puntatori, indipendentemente dal fatto che il metodo sia definito con un ricevitore di valore o un ricevitore di puntatore.

6.4 Selezione del Tipo del Ricevitore

Nel definire i metodi, si dovrebbe decidere se utilizzare un ricevitore di valore o un ricevitore di puntatore in base alla situazione. Ecco alcune linee guida comuni:

  • Se il metodo ha bisogno di modificare il contenuto della struttura, utilizzare un ricevitore di puntatore.
  • Se la struttura è grande e il costo della copia è elevato, utilizzare un ricevitore di puntatore.
  • Se si desidera che il metodo modifichi il valore a cui il ricevitore punta, utilizzare un ricevitore di puntatore.
  • Per motivi di efficienza, anche se non si modifica il contenuto della struttura, è ragionevole utilizzare un ricevitore di puntatore per una struttura grande.
  • Per strutture piccole, o quando si leggono solo dati senza la necessità di modifiche, un ricevitore di valore è spesso più semplice ed efficiente.

Attraverso i metodi della struttura, è possibile simulare alcune caratteristiche della programmazione orientata agli oggetti in Go, come l'incapsulamento e i metodi. Questo approccio in Go semplifica il concetto di oggetti pur fornendo sufficienti capacità per organizzare e gestire le funzioni correlate.

7 Struttura e Serializzazione JSON

In Go, è spesso necessario serializzare una struttura in formato JSON per la trasmissione in rete o come file di configurazione. Allo stesso modo, è anche necessario essere in grado di deserializzare JSON in istanze di struttura. Il pacchetto encoding/json in Go fornisce questa funzionalità.

Ecco un esempio su come convertire tra una struttura e JSON:

package main

import (
	"encoding/json"
	"fmt
	"log"
)

// Definire la struttura Persona, e utilizzare i tag json per definire la corrispondenza tra i campi della struttura e i nomi dei campi JSON
type Persona struct {
	Nome   string   `json:"nome"`
	Età    int      `json:"età"`
	Emails []string `json:"emails,omitempty"`
}

func main() {
	// Creare una nuova istanza di Persona
	p := Persona{
		Nome:   "Mario Rossi",
		Età:    30,
		Emails: []string{"[email protected]", "[email protected]"},
	}

	// Serializzare in JSON
	datiJSON, err := json.Marshal(p)
	if err != nil {
		log.Fatalf("La serializzazione JSON è fallita: %s", err)
	}
	fmt.Printf("Formato JSON: %s\n", datiJSON)

	// Deserializzare in una struttura
	var p2 Persona
	if err := json.Unmarshal(datiJSON, &p2); err != nil {
		log.Fatalf("La deserializzazione JSON è fallita: %s", err)
	}
	fmt.Printf("Struttura recuperata: %#v\n", p2)
}

Nel codice sopra, abbiamo definito una struttura Persona, incluso un campo di tipo slice con l'opzione "omitempty". Questa opzione specifica che se il campo è vuoto o mancante, non verrà incluso nel JSON.

Abbiamo utilizzato la funzione json.Marshal per serializzare un'istanza di struttura in JSON, e la funzione json.Unmarshal per deserializzare i dati JSON in un'istanza di struttura.

8 Argomenti Avanzati sulle Strutture

8.1 Confronto delle strutture

In Go, è consentito confrontare direttamente due istanze di strutture, ma questo confronto si basa sui valori dei campi all'interno delle strutture. Se tutti i valori dei campi sono uguali, allora le due istanze delle strutture sono considerate uguali. Va notato che non tutti i tipi di campi possono essere confrontati. Ad esempio, una struttura contenente slice non può essere confrontata direttamente.

Ecco un esempio di confronto tra strutture:

package main

import "fmt"

type Point struct {
	X, Y int
}

func main() {
	p1 := Point{1, 2}
	p2 := Point{1, 2}
	p3 := Point{1, 3}

fmt.Println("p1 == p2:", p1 == p2) // Output: p1 == p2: true
fmt.Println("p1 == p3:", p1 == p3) // Output: p1 == p3: false
}

In questo esempio, p1 e p2 sono considerati uguali perché tutti i loro valori dei campi sono gli stessi. E p3 non è uguale a p1 perché il valore di Y è diverso.

8.2 Copia delle strutture

In Go, le istanze delle strutture possono essere copiate per assegnazione. Se questa copia è una copia profonda o una copia superficiale dipende dai tipi dei campi all'interno della struttura.

Se la struttura contiene solo tipi di base (come int, stringa, ecc.), la copia è una copia profonda. Se la struttura contiene tipi di riferimento (come slice, mappe, ecc.), la copia sarà una copia superficiale e l'istanza originale e la nuova istanza copiata condivideranno la memoria dei tipi di riferimento.

Di seguito è riportato un esempio di copia di una struttura:

package main

import "fmt"

type Data struct {
Numbers []int
}

func main() {
// Inizializza un'istanza della struttura Data
originale := Data{Numbers: []int{1, 2, 3}}

// Copia la struttura
copiata := originale

// Modifica gli elementi della slice copiata
copiata.Numbers[0] = 100

// Visualizza gli elementi delle istanze originali e copiate
fmt.Println("Originale:", originale.Numbers) // Output: Originale: [100 2 3]
fmt.Println("Copiata:", copiata.Numbers) // Output: Copiata: [100 2 3]
}

Come mostrato nell'esempio, le istanze originale e copiata condividono la stessa slice, quindi la modifica dei dati della slice in copiata influenzerà anche i dati della slice in originale.

Per evitare questo problema, è possibile ottenere una vera copia profonda copiando esplicitamente i contenuti della slice in una nuova slice:

newNumbers := make([]int, len(originale.Numbers))
copy(newNumbers, originale.Numbers)
copiata := Data{Numbers: newNumbers}

In questo modo, le modifiche a copiata non influenzeranno originale.