Go non impone un framework web monolitico, ma offre una libreria standard potente e semplici primitive di concorrenza. Questo rende naturale organizzare il codice in modo esplicito, e il pattern Model-View-Controller (MVC) è uno dei modi più diffusi per strutturare applicazioni web in Go.
1. Cos'è il pattern MVC in Go
Il pattern MVC separa la logica dell'applicazione in tre parti principali:
- Model: rappresenta i dati e le regole di business (strutture, validazioni, accesso al database).
- View: gestisce la presentazione dei dati (template HTML, JSON, ecc.).
- Controller: gestisce le richieste HTTP, coordina Model e View e restituisce la risposta.
In Go, il pattern MVC non è legato a un framework specifico: possiamo applicarlo usando soltanto la libreria net/http, il package html/template e una struttura di cartelle chiara.
2. Struttura base di un progetto MVC in Go
Una possibile struttura di cartelle per una piccola applicazione potrebbe essere la seguente:
mvc-go-example/
├── main.go
├── controllers/
│ └── book_controller.go
├── models/
│ └── book.go
└── views/
└── books.html
Questa organizzazione tiene separati:
- i models, che rappresentano i dati,
- le views (template),
- i controllers, che espongono gli handler HTTP,
- il file
main.go, che configura il router e avvia il server.
3. Model: definire i dati e la logica di accesso
Nel package models definiamo le strutture che rappresentano i nostri dati e le funzioni che li recuperano. Per semplicità useremo dati in memoria, ma in un progetto reale qui collocheremmo anche le funzioni che accedono al database.
package models
type Book struct {
ID int
Title string
Author string
}
var books = []Book{
{ID: 1, Title: "Clean Code", Author: "Robert C. Martin"},
{ID: 2, Title: "The Go Programming Language", Author: "Alan A. A. Donovan"},
}
func GetAllBooks() []Book {
return books
}
func GetBookByID(id int) *Book {
for _, b := range books {
if b.ID == id {
return &b
}
}
return nil
}
Osservazioni:
Bookè il Model che rappresenta un libro.GetAllBookseGetBookByIDfungono da "repository" dei dati.- In un progetto reale queste funzioni potrebbero eseguire query su un database e gestire errori.
4. View: template HTML con html/template
Nel package views inseriamo i template HTML. Usiamo il package html/template, che effettua escaping automatico e ci protegge da injection HTML.
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<title>Libri</title>
</head>
<body>
<h1>Lista libri</h1>
<ul>
{{range .}}
<li>
<strong>{{.Title}}</strong> - {{.Author}}
</li>
{{end}}
</ul>
</body>
</html>
Note importanti:
{{range .}}itera su una slice diBook.{{.Title}}e{{.Author}}accedono ai campi della struttura.- Il template non conosce come i dati vengono recuperati: riceve solo ciò che il controller gli passa.
5. Controller: collegare HTTP, Model e View
I controller vivono nel package controllers. Ogni controller espone funzioni compatibili con http.HandlerFunc: ricevono una *http.Request, usano i models per ottenere dati, e infine scrivono la risposta (di solito delegando a una view).
package controllers
import (
"net/http"
"html/template"
"mvc-go-example/models"
)
var booksTemplate = template.Must(template.ParseFiles("views/books.html"))
func ListBooks(w http.ResponseWriter, r *http.Request) {
data := models.GetAllBooks()
if err := booksTemplate.Execute(w, data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
Qui il controller fa tre cose fondamentali:
- Chiama
models.GetAllBooks()per ottenere i dati. - Passa i dati al template
booksTemplate. - Gestisce eventuali errori restituendo un
500 Internal Server Errorse il template non può essere renderizzato.
6. main.go: configurare router e server HTTP
Il file main.go collega il router HTTP ai controller e avvia il server web.
package main
import (
"log"
"net/http"
"mvc-go-example/controllers"
)
func main() {
http.HandleFunc("/books", controllers.ListBooks)
log.Println("Server in ascolto su http://localhost:8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
Quando un utente visita /books, la richiesta viene instradata alla funzione controllers.ListBooks, che a sua volta usa Model e View per produrre la risposta.
7. Estendere la struttura MVC
Man mano che l'applicazione cresce, possiamo raffinare la separazione delle responsabilità aggiungendo livelli intermedi.
- Services: un livello di servizio che incapsula la logica di business più complessa e usa i models per accedere ai dati.
- Router dedicati: al posto di
http.HandleFuncpossiamo usare router come chi o altri per una gestione più avanzata delle rotte. - Package per la configurazione: per tenere separati dettagli di configurazione (porta, variabili d'ambiente) dal resto del codice.
Un esempio semplificato con un livello di servizio potrebbe essere:
// Esempio di separazione ulteriore con un livello "service"
type BookService struct{}
func (BookService) List() []Book {
return GetAllBooks()
}
In questo caso il controller non dipenderebbe direttamente dalle funzioni del Model, ma da un BookService che incapsula la logica.
8. Buone pratiche per MVC in Go
- Mantenere i controller leggeri: dovrebbero solo orchestrare Model e View, non contenere logica di business pesante.
- Tenere i models indipendenti dal trasporto (HTTP, gRPC): possono essere riutilizzati in altre interfacce.
- Evitare di mescolare codice HTML nei controller: usare sempre i template per la presentazione.
- Scrivere test per models e services: sono le parti dove la logica è più concentrata.
- Organizzare il codice per package e responsabilità, non per tipo di file.
Applicare il pattern MVC in Go non significa aderire a un framework rigido, ma scegliere una struttura che renda il codice leggibile, testabile ed estendibile. Partendo da una semplice separazione in models, views e controllers, è possibile evolvere il progetto verso una architettura più ricca senza perdere la semplicità tipica di Go.