Usare il pattern MVC in Go

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.
  • GetAllBooks e GetBookByID fungono 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 di Book.
  • {{.Title}} e {{.Author}} accedono ai campi della struttura.
  • Il template non conosce come i dati vengono recuperati: riceve solo ci&ograve; 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:

  1. Chiama models.GetAllBooks() per ottenere i dati.
  2. Passa i dati al template booksTemplate.
  3. Gestisce eventuali errori restituendo un 500 Internal Server Error se il template non pu&ograve; 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&ugrave; complessa e usa i models per accedere ai dati.
  • Router dedicati: al posto di http.HandleFunc possiamo usare router come chi o altri per una gestione pi&ugrave; 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&ugrave; 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&ugrave; ricca senza perdere la semplicità tipica di Go.

Torna su