Go, visto da chi scrive JavaScript tutto il giorno
Non è la velocità del benchmark. È un binario da 12 MB che parte in 8 millisecondi, usa 10 MB di RAM e resta lì per mesi. Cosa cambia davvero quando il backend smette di essere JavaScript — e cosa si perde.
Per anni ho evitato Go per una ragione che oggi mi sembra sciocca: "non ha i generics" — e poi li ha avuti — "è verboso" — ed è vero — "non è JavaScript". L'ultima era quella vera. Quando lavori in un ecosistema da dieci anni, ogni altro linguaggio sembra un trasloco.
Poi ho dovuto far girare un servizio che doveva stare sotto un certo budget di memoria, reggere qualche migliaio di connessioni aperte e non riavviarsi mai. L'ho riscritto in Go in tre giorni, e da allora guardo i numeri in modo diverso.
I numeri che non sono benchmark
La cosa che colpisce non è il throughput — su una singola richiesta semplice Node, Bun e Go sono tutti "abbastanza veloci" e le differenze si perdono nel rumore della rete. Sono altri tre numeri, e sono tutti noiosi.
Il binario. go build produce un eseguibile di 10–15 MB che contiene tutto: runtime, librerie, il tuo codice. Niente node_modules, niente versione del runtime da allineare sul server, niente npm install in produzione che fallisce perché una dipendenza transitiva è sparita dal registry. Lo copi, lo avvii, funziona. Su una macchina dove non hai installato niente.
Il tempo di avvio. Otto, dieci millisecondi. Un processo Node con un framework web carica centinaia di moduli prima di rispondere alla prima richiesta — da un centinaio di millisecondi a qualche secondo, a seconda di quanto è grande l'albero. Non importa quasi mai, finché non importa: un deploy senza downtime, un crash con restart, uno scaling orizzontale sotto picco. Lì un processo che è pronto subito cambia la forma dell'incidente.
La memoria a riposo. Un server Go che ascolta su una porta e non fa niente sta sotto i 10 MB. Sotto carico cresce con i dati che tiene davvero, e il garbage collector li restituisce. Non c'è un --max-old-space-size da indovinare, non c'è un riavvio notturno da programmare perché "la memoria sale". Sta lì. Per mesi.
Il modello che cambia le decisioni
La differenza più profonda non è nei numeri ma in una parola: go. Una goroutine costa qualche kilobyte e il runtime le schedula su tutti i core della macchina. Questo significa che un lavoro lungo — comprimere una risposta, calcolare un hash costoso, leggere un file da 200 MB — non blocca nessun altro. Non devi spostarlo in un worker, non devi spezzarlo in pezzi piccoli per non monopolizzare l'event loop, non devi spiegare a nessuno perché il timer del keep-alive è scattato in ritardo.
Chi viene da JavaScript ha passato anni a imparare a non fare certe cose nel thread principale. In Go quella disciplina semplicemente non serve, e ci vuole un po' per fidarsi.
Cosa si perde, onestamente
Sarebbe disonesto fermarsi qui.
Si perde la lingua comune tra server e browser. Un tipo definito in Go non esiste in TypeScript finché qualcuno non lo genera — e la generazione è un punto di rottura: se non è esatta, se dice string dove il server accetta anche null, il tipo mente e hai perso proprio la cosa per cui lo volevi. Ho imparato sulla mia pelle che "quasi esatto" in un tipo generato è peggio di nessun tipo: ti fidi, e ti sbagli.
Si perde il rendering di componenti. Go non ha un modello di componenti paragonabile a React, e i tentativi di imitarlo sono tutti peggio dell'originale. Se la tua pagina è fatta di componenti, il posto dove renderizzarla resta JavaScript.
Si perde l'immediatezza. Un'espressione che in TypeScript sta in una riga, in Go ne occupa sei, con un if err != nil in mezzo. È voluto — gli errori sono valori, e li devi guardare — ma le prime settimane sembrano burocrazia.
Quindi cosa ci faccio
La conclusione a cui sono arrivato non è "Go al posto di JavaScript". È Go dove serve un server, JavaScript dove serve una pagina, e una cucitura stretta e tipizzata tra i due. Il frontend renderizza e idrata; il backend persiste, decide, fa streaming e non si riavvia. Ognuno fa la cosa per cui è stato progettato, e nessuno dei due deve fingere di essere l'altro.
È meno elegante di "un linguaggio per tutto". Ma "un linguaggio per tutto" non era eleganza: era un rinvio, e i numeri noiosi qui sopra sono il conto che arriva.