Usare il pattern MVC in Go

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.

Go di Gabriele Romanato

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.