Cos'è Go e perché è nato in Google
Cos'è Go (Golang): la semplicità deliberata, le goroutine per la concorrenza, il binario unico che semplifica il deploy, e i limiti reali del linguaggio.
Hai provato a capire perché quasi tutti gli strumenti moderni di infrastruttura — Docker, Kubernetes, Terraform — sono scritti nello stesso linguaggio, e la risposta che trovi è sempre vaga: "è veloce", "è fatto per il cloud". In questo articolo trovi il motivo vero, che non è la velocità ma una scelta di progetto insolita: togliere funzionalità invece di aggiungerne.
Cos'è Go
Go è un linguaggio compilato creato in Google nel 2009 per scrivere servizi di rete, progettato deliberatamente con poche funzionalità in modo che il codice resti leggibile anche quando lo scrivono centinaia di persone diverse.
Il nome ufficiale è Go, "Golang" è il termine con cui lo si cerca online perché "go" da solo è una parola inutilizzabile in un motore di ricerca.
Nasce da un problema concreto e molto noioso: in Google i tempi di compilazione del C++ erano diventati insopportabili, il codice era pieno di costruzioni che ogni team interpretava a modo suo, e formare una persona nuova su un progetto costava mesi. I tre autori — Rob Pike, Ken Thompson e Robert Griesemer — non hanno cercato di fare un linguaggio più potente. Hanno cercato di farne uno più noioso.
La semplicità è la caratteristica, non un compromesso
Questa è la parte che disorienta chi arriva da altri linguaggi. Go non ha ereditarietà tra classi, non ha eccezioni, non ha overload degli operatori, non ha valori predefiniti per i parametri, per anni non ha avuto nemmeno i generici (arrivati solo nel 2022, con cautela).
Sono assenze volute. L'idea è che esista una sola maniera ragionevole di fare ogni cosa, così il codice scritto da altri si legge senza doverlo decifrare.
Questo si vede in dettagli piccoli ma rivelatori:
- La formattazione non si discute:
gofmtriscrive il codice in modo canonico, e tutti lo usano. Non esistono guerre sull'indentazione. - Una variabile dichiarata e mai usata è un errore di compilazione, non un avviso. Il programma non compila.
- Un import non usato, idem.
La prima settimana questa rigidità irrita. Dopo un mese ti accorgi che stai leggendo il codice di sconosciuti senza fatica, ed è esattamente ciò che volevano ottenere.
Il costo è reale e va detto: Go è meno espressivo di quasi tutti i suoi coetanei. Certe cose che in Python scrivi in una riga qui ne richiedono otto. Chi arriva da linguaggi ricchi lo trova povero — ed è una critica legittima, non un malinteso.
Il binario unico: perché è enorme
Quando compili un programma Go ottieni un singolo file eseguibile che contiene tutto, libreria standard inclusa. Nessun runtime da installare, nessuna macchina virtuale, nessun node_modules, nessun interprete della versione giusta.
go build -o miosito ./cmd/server
scp miosito server:/usr/local/bin/
Fine. Il programma gira.
Confronta con il deploy di un'applicazione Node.js o Python: devi avere l'interprete nella versione corretta, installare le dipendenze sul server, gestire ambienti virtuali, e sperare che la macchina di produzione somigli alla tua. Qui quel problema non esiste.
Nei container l'effetto è ancora più evidente. Un'immagine Docker per un servizio Go può partire da zero:
FROM golang:1.23 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server
FROM scratch
COPY --from=build /app /app
ENTRYPOINT ["/app"]
FROM scratch significa immagine vuota: nessun sistema operativo dentro. Il risultato pesa quindici megabyte invece di trecento, parte in millisecondi e ha una superficie di attacco minuscola — non c'è nemmeno una shell da cui un attaccante possa partire.
Aggiungi che la compilazione incrociata è banale (GOOS=linux GOARCH=arm64 go build) e capisci perché gli strumenti a riga di comando distribuiti su tre sistemi operativi tendono a essere scritti in Go.
Le goroutine: la concorrenza senza il mal di testa
Il secondo motivo per cui Go esiste è la concorrenza, cioè far succedere più cose contemporaneamente. Un server web deve servire migliaia di richieste insieme, e il modo tradizionale — un thread del sistema operativo per ogni richiesta — costa caro: ogni thread occupa memoria e il sistema fatica oltre qualche migliaio.
Una goroutine è una funzione che parte per conto suo, gestita dal runtime di Go e non dal sistema operativo. Costa pochi kilobyte, e averne centomila attive è normale.
Farla partire richiede una parola:
go inviaEmail(utente)
La funzione parte, il programma prosegue senza aspettarla.
Il problema di tutti i sistemi concorrenti è far comunicare queste esecuzioni parallele senza che si pestino i piedi. Go propone i channel: tubi tipizzati in cui una goroutine scrive e un'altra legge.
func main() {
risultati := make(chan string)
siti := []string{"https://esempio.it", "https://altro.it"}
for _, url := range siti {
go func(u string) {
resp, err := http.Get(u)
if err != nil {
risultati <- u + ": errore"
return
}
defer resp.Body.Close()
risultati <- fmt.Sprintf("%s: %d", u, resp.StatusCode)
}(url)
}
for range siti {
fmt.Println(<-risultati)
}
}
Le due richieste partono insieme. <-risultati aspetta che arrivi qualcosa nel canale. Il tempo totale è quello della richiesta più lenta, non la somma.
Il principio dietro si riassume in una frase degli autori: non comunicare condividendo memoria, condividi memoria comunicando. Invece di mettere un dato in una variabile globale protetta da un lucchetto, lo passi dentro un canale. Il passaggio di proprietà è esplicito e diventa molto più difficile sbagliare.
Non è magia: le goroutine si possono comunque bloccare a vicenda, i canali si possono riempire, e un deadlock è ancora possibile. Ma il modello mentale è alla portata di chiunque dopo qualche giorno, e questo è il punto.
if err != nil, la critica più frequente
In Go non esistono le eccezioni. Una funzione che può fallire restituisce due valori: il risultato e un errore.
dati, err := os.ReadFile("config.json")
if err != nil {
return fmt.Errorf("lettura config: %w", err)
}
var cfg Config
if err := json.Unmarshal(dati, &cfg); err != nil {
return fmt.Errorf("parsing config: %w", err)
}
Sì: lo scrivi ovunque. In un file di trecento righe quel blocco compare quaranta volte, e è la cosa più criticata del linguaggio. La critica è fondata: è verboso, e leggere codice Go significa saltare visivamente un sacco di controlli quasi identici.
La difesa, che trovo onesta anch'essa: ogni punto in cui qualcosa può andare storto è visibile nel codice. Con le eccezioni, una funzione può esplodere da tre livelli di profondità e tu non lo sai finché non succede in produzione. Qui il percorso dell'errore è scritto nero su bianco, e il %w costruisce una catena leggibile — "avvio server: lettura config: file non trovato" — invece di uno stack trace da interpretare.
È un compromesso: verbosità in cambio di chiarezza. Se preferisci la concisione, Go ti starà stretto e nessuno può convincerti del contrario.
Dove si usa davvero (e dove no)
| Ambito | Adatto? | Perché |
|---|---|---|
| Servizi di rete, API | Molto | Concorrenza nativa, libreria HTTP standard ottima |
| Infrastruttura e DevOps | Molto | Docker e Kubernetes sono in Go |
| Strumenti a riga di comando | Molto | Un binario per piattaforma, zero dipendenze |
| Microservizi e container | Molto | Immagini minuscole, avvio istantaneo |
| Interfacce grafiche | No | Ecosistema immaturo, nessuna soluzione standard |
| Calcolo scientifico, AI | No | Le librerie sono in Python, e lo resteranno |
| Web frontend | No | Il browser esegue JavaScript |
| Prototipi rapidi e script | Poco | Python fa lo stesso in meno righe |
Che Docker e Kubernetes siano scritti in Go non è un aneddoto: ha creato un effetto valanga. Chi lavora su container e orchestrazione incontra Go per forza, perché le librerie di quel mondo sono lì.
Errori comuni che fanno perdere ore
Catturare la variabile del ciclo dentro una goroutine. Il classico storico: prima di Go 1.22 tutte le goroutine vedevano l'ultimo valore. Dalla 1.22 il comportamento è stato corretto, ma trovi ancora montagne di codice e di tutorial scritti per il vecchio modello — e passare il valore come parametro, come nell'esempio sopra, resta l'abitudine sicura.
Dimenticare di chiudere il corpo delle risposte HTTP. Senza defer resp.Body.Close() le connessioni non vengono rilasciate: il programma funziona benissimo per due giorni e poi esaurisce i descrittori di file.
Scrivere su un canale che nessuno legge. La goroutine si blocca per sempre e non se ne accorge nessuno. È la versione Go della perdita di memoria, e si chiama proprio "goroutine leak".
Portare l'ereditarietà da Java. Go non ce l'ha e non la vuole: si compone incorporando strutture e si programma verso le interfacce. Chi insiste a replicare gerarchie di classi combatte col linguaggio e perde.
Usare panic come fosse un'eccezione. Serve per situazioni irrecuperabili, non per il controllo di flusso normale.
In sintesi
Go è un linguaggio volutamente piccolo, nato per risolvere problemi di scala organizzativa prima che tecnici: compilare in fretta, leggersi senza ambiguità, formarci una persona nuova in una settimana.
Le tre ragioni concrete per sceglierlo sono il binario unico che rende il deploy banale e le immagini container minuscole, le goroutine che rendono la concorrenza accessibile, e una libreria standard che copre da sola quasi tutto ciò che serve a un servizio di rete.
I limiti sono reali e non vanno nascosti: la gestione degli errori è verbosa, il linguaggio è meno espressivo di quasi tutti i suoi pari, e fuori dal suo territorio — interfacce grafiche, calcolo scientifico, frontend — semplicemente non ha senso usarlo.
Quando ha senso impararlo: se lavori su backend, infrastruttura o strumenti di sistema. Non come primo linguaggio in assoluto — per quello vedi come imparare a programmare da zero — ma come secondo o terzo è tra gli investimenti col miglior rapporto tra tempo speso e resa.
Se stai confrontando alternative, il quadro completo è in tutti i linguaggi di programmazione, e il confronto diretto con l'altro linguaggio compilato del momento è in cos'è Rust.