Metodi in Go
I metodi in Go sono funzioni associate a un tipo specifico tramite un parametro speciale chiamato receiver. A differenza dei linguaggi orientati agli oggetti tradizionali, in Go i metodi non vengono dichiarati all’interno di una classe, ma sono funzioni con un receiver che indica su quale tipo operano. In questa guida esploreremo i metodi, i tipi di receiver e le best practice.
Definizione di un Metodo
Un metodo e una funzione con un parametro receiver tra la parola chiave func e il nome del metodo.
package main
import "fmt"
type Rettangolo struct {
Larghezza float64
Altezza float64
}
// Metodo con value receiver
func (r Rettangolo) Area() float64 {
return r.Larghezza * r.Altezza
}
func (r Rettangolo) Perimetro() float64 {
return 2 * (r.Larghezza + r.Altezza)
}
func main() {
ret := Rettangolo{Larghezza: 5, Altezza: 3}
fmt.Printf("Area: %.1f\n", ret.Area()) // Area: 15.0
fmt.Printf("Perimetro: %.1f\n", ret.Perimetro()) // Perimetro: 16.0
}
Il receiver (r Rettangolo) indica che il metodo appartiene al tipo Rettangolo. La variabile r e la copia dell’istanza su cui viene chiamato il metodo.
Value Receiver vs Pointer Receiver
La scelta tra value receiver e pointer receiver e fondamentale in Go.
Value Receiver
Il value receiver lavora su una copia del valore. Le modifiche non influenzano l’originale.
type Contatore struct {
Valore int
}
// Value receiver: lavora su una copia
func (c Contatore) IncrementaSbagliato() {
c.Valore++ // Modifica la copia, non l'originale
}
Pointer Receiver
Il pointer receiver lavora sul valore originale, permettendo le modifiche.
// Pointer receiver: modifica l'originale
func (c *Contatore) Incrementa() {
c.Valore++
}
func (c *Contatore) Reset() {
c.Valore = 0
}
func main() {
c := Contatore{Valore: 0}
c.Incrementa()
c.Incrementa()
c.Incrementa()
fmt.Println(c.Valore) // 3
c.Reset()
fmt.Println(c.Valore) // 0
}
Go converte automaticamente tra valore e puntatore quando si chiama un metodo: c.Incrementa() viene automaticamente interpretato come (&c).Incrementa().
Quando Usare Pointer Receiver
Usa un pointer receiver quando:
- Il metodo deve modificare il receiver
- La struct e grande e vuoi evitare la copia
- Per coerenza: se un metodo del tipo usa pointer receiver, tutti gli altri dovrebbero farlo
type Utente struct {
Nome string
Email string
Attivo bool
}
// Tutti i metodi usano pointer receiver per coerenza
func (u *Utente) Disattiva() {
u.Attivo = false
}
func (u *Utente) CambiaEmail(nuovaEmail string) {
u.Email = nuovaEmail
}
func (u *Utente) String() string {
stato := "attivo"
if !u.Attivo {
stato = "inattivo"
}
return fmt.Sprintf("%s (%s) - %s", u.Nome, u.Email, stato)
}
Metodi su Qualsiasi Tipo
I metodi possono essere definiti su qualsiasi tipo definito nel pacchetto corrente, non solo sulle struct.
package main
import "fmt"
type Celsius float64
type Fahrenheit float64
func (c Celsius) InFahrenheit() Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
func (f Fahrenheit) InCelsius() Celsius {
return Celsius((f - 32) * 5 / 9)
}
func main() {
temp := Celsius(100)
fmt.Printf("%.1f C = %.1f F\n", temp, temp.InFahrenheit())
// 100.0 C = 212.0 F
tempF := Fahrenheit(72)
fmt.Printf("%.1f F = %.1f C\n", tempF, tempF.InCelsius())
// 72.0 F = 22.2 C
}
Non si possono definire metodi su tipi provenienti da altri pacchetti. Per farlo, bisogna creare un tipo locale basato su quello esterno.
Method Set (Insieme di Metodi)
Il method set di un tipo determina quali interfacce puo soddisfare:
- Il method set di un valore
Tinclude solo i metodi con value receiver(t T) - Il method set di un puntatore
*Tinclude sia i metodi con value receiver che quelli con pointer receiver
type Forma interface {
Area() float64
}
type Cerchio struct {
Raggio float64
}
func (c Cerchio) Area() float64 {
return 3.14159 * c.Raggio * c.Raggio
}
func main() {
c := Cerchio{Raggio: 5}
var f Forma
f = c // OK: Cerchio ha il metodo Area() con value receiver
f = &c // OK: *Cerchio include i metodi di Cerchio
fmt.Println(f.Area())
}
Se Area() avesse un pointer receiver, solo &c (non c) potrebbe essere assegnato a f.
Espressioni di Metodo (Method Expressions)
Un metodo puo essere usato come valore di funzione tramite le method expressions.
type Calcolatrice struct{}
func (c Calcolatrice) Somma(a, b int) int { return a + b }
func (c Calcolatrice) Prodotto(a, b int) int { return a * b }
func main() {
calc := Calcolatrice{}
// Method expression: il receiver diventa il primo parametro
somma := Calcolatrice.Somma
risultato := somma(calc, 5, 3)
fmt.Println(risultato) // 8
// Method value: il receiver e gia legato
prodotto := calc.Prodotto
fmt.Println(prodotto(4, 7)) // 28
}
Le method expressions sono utili quando si devono passare metodi come argomenti a funzioni di ordine superiore.
Metodi con Embedding
Quando una struct incorpora un’altra struct, i metodi della struct incorporata vengono “promossi” e sono accessibili direttamente.
type Logger struct{}
func (l Logger) Log(messaggio string) {
fmt.Println("[LOG]", messaggio)
}
type Server struct {
Logger // Embedding
Porta int
}
func main() {
s := Server{Porta: 8080}
s.Log("Server avviato") // Chiama Logger.Log direttamente
}
Pattern Builder con Metodi
Un pattern comune e il builder con metodi che restituiscono il puntatore al receiver per permettere il concatenamento.
type QueryBuilder struct {
tabella string
condizioni []string
limite int
}
func (q *QueryBuilder) Da(tabella string) *QueryBuilder {
q.tabella = tabella
return q
}
func (q *QueryBuilder) Dove(condizione string) *QueryBuilder {
q.condizioni = append(q.condizioni, condizione)
return q
}
func (q *QueryBuilder) Limite(n int) *QueryBuilder {
q.limite = n
return q
}
func main() {
query := (&QueryBuilder{}).
Da("utenti").
Dove("eta > 18").
Dove("attivo = true").
Limite(10)
fmt.Printf("Tabella: %s, Condizioni: %v, Limite: %d\n",
query.tabella, query.condizioni, query.limite)
}
Conclusione
I metodi in Go offrono un modo elegante per associare comportamento ai tipi. La scelta tra value receiver e pointer receiver e una decisione di design importante: usa pointer receiver quando il metodo modifica il receiver o la struct e grande. Il method set determina quali interfacce un tipo soddisfa. L’embedding promuove automaticamente i metodi, offrendo composizione anziche ereditarieta. Comprendere i metodi e propedeutico per padroneggiare le interfacce in Go.