Domande da colloquio backend Go: guida pratica

Milad Bonakdar
Autore
Preparati ai colloqui backend Go con domande su goroutine, channel, context, gestione degli errori, test, server HTTP e system design.
Introduzione
Nei colloqui backend Go di solito si valuta se sai costruire servizi semplici e affidabili: goroutine e channel, cancellazione con context, gestione esplicita degli errori, test, middleware HTTP e compromessi di progettazione. Una buona risposta spiega sia cosa fa uno strumento sia quando è meglio non usarlo.
Usa questa guida per preparare risposte concise e collega ogni tema a un progetto reale: un'API sviluppata, un worker pool debuggato, una race condition risolta o un servizio spento in modo sicuro durante un deploy.
Concetti di base di Go
1. Cosa rende Go diverso da altri linguaggi come Java o Python?
Risposta: Go è pensato per codice di sistema leggibile e manutenibile. In un colloquio, una risposta solida dovrebbe citare:
- Semplicità: Go evita ereditarietà e overload dei metodi; i team usano composizione, interfacce piccole e confini chiari tra package.
- Concorrenza: Goroutine, channel, primitive
syncecontextrendono esplicito il lavoro concorrente nel backend. - Compilazione: Go produce binari nativi con avvio rapido e deployment semplice.
- Libreria standard: Pacchetti come
net/http,context,encoding/jsonetestingcoprono molto lavoro backend prima di introdurre framework.
Rarità: Comune Difficoltà: Facile
2. Spiega la differenza tra Array e Slice.
Risposta:
- Array: Sequenze di dimensioni fisse di elementi dello stesso tipo. La dimensione fa parte del tipo (ad esempio,
[5]intè diverso da[10]int). Sono tipi di valore; assegnare un array a un altro copia tutti gli elementi. - Slice: Viste dinamiche e flessibili di un array sottostante. Sono costituiti da un puntatore all'array, una lunghezza e una capacità. Le slice sono simili a riferimenti; passare una slice a una funzione consente la modifica degli elementi sottostanti senza copiare l'intero set di dati.
Rarità: Comune Difficoltà: Facile
3. Come funzionano le Interfacce in Go? Cos'è l'implementazione implicita?
Risposta: Le interfacce in Go sono raccolte di firme di metodi.
- Implementazione implicita: A differenza di Java o C#, un tipo non dichiara esplicitamente di implementare un'interfaccia (nessuna parola chiave
implements). Se un tipo definisce tutti i metodi dichiarati in un'interfaccia, implementa automaticamente tale interfaccia. - Duck Typing: "Se cammina come un'anatra e starnazza come un'anatra, è un'anatra". Questo disaccoppia la definizione dall'implementazione, rendendo il codice più flessibile e più facile da simulare per i test.
Rarità: Comune Difficoltà: Media
4. Cos'è la parola chiave defer e come funziona?
Risposta:
defer pianifica una chiamata di funzione da eseguire immediatamente prima che la funzione ritorni. Viene comunemente utilizzato per la pulizia delle risorse, come la chiusura di file, lo sblocco di mutex o la chiusura di connessioni al database.
- Ordine LIFO: Le chiamate differite vengono eseguite in ordine Last-In-First-Out.
- Valutazione degli argomenti: Gli argomenti delle funzioni differite vengono valutati quando viene eseguita l'istruzione
defer, non quando viene eseguita la chiamata.
Esempio:
Rarità: Comune Difficoltà: Facile
Concorrenza
5. Spiega le Goroutine e in cosa differiscono dai thread del sistema operativo.
Risposta:
- Goroutine: Thread leggeri gestiti dal runtime di Go. Iniziano con uno stack piccolo (ad esempio, 2 KB) che cresce e si riduce dinamicamente. Migliaia di goroutine possono essere eseguite su un singolo thread del sistema operativo.
- Thread del sistema operativo: Gestiti dal kernel, hanno stack fissi di grandi dimensioni (ad esempio, 1 MB) e il cambio di contesto è costoso.
- Pianificazione M:N: Il runtime di Go multiplexa M goroutine su N thread del sistema operativo, gestendo la pianificazione in modo efficiente nello spazio utente.
Rarità: Molto comune Difficoltà: Media
6. Cosa sono i Canali? Bufferizzati vs. Non bufferizzati?
Risposta: I canali sono condotti tipizzati che consentono alle goroutine di comunicare e sincronizzare l'esecuzione.
- Canali non bufferizzati: Non hanno capacità. Un'operazione di invio si blocca finché un ricevitore non è pronto e viceversa. Forniscono una forte sincronizzazione.
- Canali bufferizzati: Hanno una capacità. Un'operazione di invio si blocca solo se il buffer è pieno. Un'operazione di ricezione si blocca solo se il buffer è vuoto. Disaccoppiano in qualche modo mittente e destinatario.
Rarità: Comune Difficoltà: Media
7. Come gestisci le Race Condition in Go?
Risposta: Una race condition si verifica quando più goroutine accedono contemporaneamente alla memoria condivisa e almeno un accesso è una scrittura.
- Rilevamento: Utilizzare il rilevatore di race integrato:
go run -raceogo test -race. - Prevenzione:
- Canali: "Non comunicare condividendo la memoria; invece, condividi la memoria comunicando."
- Pacchetto Sync: Utilizzare
sync.Mutexosync.RWMutexper bloccare le sezioni critiche. - Operazioni atomiche: Utilizzare
sync/atomicper contatori o flag semplici.
Rarità: Comune Difficoltà: Difficile
8. A cosa serve l'istruzione select?
Risposta:
L'istruzione select consente a una goroutine di attendere più operazioni di comunicazione. Si blocca finché uno dei suoi casi non può essere eseguito, quindi esegue quel caso. Se più casi sono pronti, ne sceglie uno a caso.
- Timeout: Può essere implementato utilizzando
time.After. - Operazioni non bloccanti: Un caso
defaultrende la selezione non bloccante se nessun altro caso è pronto.
Esempio:
Rarità: Media Difficoltà: Media
Gestione degli errori e robustezza
9. Come funziona la gestione degli errori in Go?
Risposta:
Go tratta gli errori come valori. Le funzioni restituiscono un tipo error (di solito come ultimo valore restituito) invece di generare eccezioni.
- Controlla errori: I chiamanti devono controllare esplicitamente se l'errore è
nil. - Errori personalizzati: È possibile creare tipi di errore personalizzati implementando l'interfaccia
error(che ha un singolo metodoError() string). - Wrapping: Go 1.13 ha introdotto il wrapping degli errori (
fmt.Errorf("%w", err)) per aggiungere contesto preservando l'errore originale per l'ispezione (utilizzandoerrors.Iseerrors.As).
Rarità: Comune Difficoltà: Facile
10. Cosa sono Panic e Recover? Quando dovresti usarli?
Risposta:
- Panic: Interrompe il normale flusso di controllo e inizia a generare panico. È simile a un'eccezione, ma dovrebbe essere riservato a errori non recuperabili (ad esempio, dereferenziazione di puntatore nullo, indice fuori dai limiti).
- Recover: Una funzione integrata che riprende il controllo di una goroutine in panico. È utile solo all'interno di una funzione
defer. - Utilizzo: Generalmente sconsigliato per il normale flusso di controllo. Utilizzare i valori
errorper le condizioni di errore previste. Panic/recover viene utilizzato principalmente per situazioni veramente eccezionali o all'interno di librerie/framework per impedire a un arresto anomalo di abbattere l'intero server.
Rarità: Media Difficoltà: Media
Progettazione del sistema e backend
11. Come struttureresti un'applicazione web Go?
Risposta: Sebbene Go non imponga una struttura, uno standard comune è il "Layout del progetto Go standard":
/cmd: Applicazioni principali (punti di ingresso)./pkg: Codice della libreria che può essere utilizzato da applicazioni esterne./internal: Codice privato dell'applicazione e della libreria (applicato dal compilatore Go)./api: Specifiche OpenAPI/Swagger, definizioni di protocollo./configs: File di configurazione.- Architettura pulita: Separare le preoccupazioni in livelli (Delivery/Handler, Usecase/Service, Repository/Data) per rendere l'app testabile e manutenibile.
Rarità: Comune Difficoltà: Media
12. Come funziona il pacchetto context e perché è importante?
Risposta:
Il pacchetto context trasporta deadline, segnali di cancellazione e valori legati alla richiesta attraverso API e goroutine.
- Cancellazione: Se il client si disconnette o un'operazione padre viene annullata, il lavoro a valle dovrebbe osservare
ctx.Done()e fermarsi. - Timeout: Usa
context.WithTimeoutocontext.WithDeadlineper query al database e chiamate esterne, e chiama la funzione cancel restituita, spesso condefer cancel(). - Valori: Conserva solo metadati della richiesta, non parametri opzionali o oggetti grandi.
Rarità: Molto comune Difficoltà: Difficile
13. Cos'è l'iniezione delle dipendenze e come viene eseguita in Go?
Risposta: L'iniezione delle dipendenze (DI) è un modello di progettazione in cui un oggetto riceve altri oggetti da cui dipende.
- In Go: Solitamente implementato passando le dipendenze (come una connessione al database o un logger) nel costruttore o nella funzione factory di uno struct, spesso tramite interfacce.
- Vantaggi: Rende il codice più modulare e testabile (facile sostituire il database reale con un mock).
- Framework: Sebbene la DI manuale sia preferibile per semplicità, esistono librerie come
google/wireouber-go/digper grafici complessi.
Rarità: Media Difficoltà: Media
Database e strumenti
14. Come gestisci JSON in Go?
Risposta:
Go utilizza il pacchetto encoding/json.
- Tag Struct: Utilizzare tag come
`json:"field_name"`per mappare i campi dello struct alle chiavi JSON. - Marshal: Converte uno struct Go in una stringa JSON (slice di byte).
- Unmarshal: Analizza i dati JSON in uno struct Go.
- Streaming:
json.Decoderejson.Encodersono migliori per payload di grandi dimensioni poiché elaborano flussi di dati.
Rarità: Comune Difficoltà: Facile
15. Quali sono alcuni strumenti Go comuni che utilizzi?
Risposta:
go mod: Gestione delle dipendenze.go fmt: Formatta il codice in stile standard.go vet: Esamina il codice alla ricerca di costrutti sospetti.go test: Esegue test e benchmark.pprof: Strumento di profilazione per l'analisi dell'utilizzo di CPU e memoria.delve: Debugger per Go.
Rarità: Comune Difficoltà: Facile
Argomenti avanzati e migliori pratiche
16. Cosa sono i Generics in Go e quando dovresti usarli?
Risposta: I Generics (introdotti in Go 1.18) consentono di scrivere funzioni e strutture di dati che funzionano con qualsiasi insieme di tipi, anziché con un tipo specifico.
- Parametri di tipo: Definiti utilizzando parentesi quadre
[]. ad esempio,func Map[K comparable, V any](m map[K]V) ... - Vincoli: Interfacce che definiscono l'insieme di tipi ammissibili (ad esempio,
any,comparable). - Utilizzo: Usa i generics quando lo stesso algoritmo o la stessa struttura dati è davvero indipendente dal tipo, per esempio set, helper o utility per collezioni. Parti da codice normale; aggiungi parametri di tipo quando la duplicazione diventa reale. Se una piccola interfaccia comunica meglio il comportamento, preferisci l'interfaccia.
Rarità: Comune Difficoltà: Media
17. Spiega i Table-Driven Tests in Go.
Risposta: Il table-driven testing è un modello preferito in Go in cui i casi di test sono definiti come una slice di struct (la "tabella"). Ogni struct contiene gli argomenti di input e l'output previsto.
- Vantaggi:
- Netta separazione della logica di test e dei dati di test.
- Facile aggiungere nuovi casi di test (basta aggiungere una riga alla tabella).
- Messaggi di errore chiari che mostrano esattamente quale input non è riuscito.
- Esempio:
Rarità: Comune Difficoltà: Facile
18. Cos'è il Middleware Pattern nei server HTTP Go?
Risposta:
Il middleware è una funzione che racchiude un http.Handler per eseguire la logica di pre- o post-elaborazione prima di passare il controllo al gestore successivo.
- Firma:
func(next http.Handler) http.Handler - Casi d'uso: Logging, Autenticazione, Panic Recovery, Rate Limiting, CORS.
- Concatenamento: Il middleware può essere concatenato (ad esempio,
Log(Auth(Handler))).
Esempio:
Rarità: Molto comune Difficoltà: Media
19. Come implementi lo spegnimento graduale in un server Go?
Risposta: Lo spegnimento graduale garantisce che un server smetta di accettare nuove richieste ma termini l'elaborazione delle richieste attive prima di uscire.
- Meccanismo:
- Ascolta i segnali del sistema operativo (SIGINT, SIGTERM) utilizzando
os/signal. - Crea un
context.WithTimeoutper consentire una finestra di pulizia (ad esempio, 5-10 secondi). - Chiama
server.Shutdown(ctx)sull'http.Server. - Chiudi le connessioni DB e altre risorse.
- Ascolta i segnali del sistema operativo (SIGINT, SIGTERM) utilizzando
- Importanza: Impedisce la perdita di dati e gli errori del client durante le implementazioni.
Rarità: Comune Difficoltà: Difficile
20. Quando dovresti usare sync.Map invece di una normale mappa con un Mutex?
Risposta:
sync.Map è un'implementazione di mappa thread-safe nella libreria standard.
- Casi d'uso:
- Cache Contention: Quando la voce per una determinata chiave viene scritta solo una volta ma letta molte volte (ad esempio, cache a caricamento lento).
- Chiavi disgiunte: Quando più goroutine leggono, scrivono e sovrascrivono voci per insiemi di chiavi disgiunti.
- Compromesso: Per i casi d'uso generali (aggiornamenti di lettura/scrittura frequenti), una normale
mapprotetta dasync.RWMutexè spesso più veloce e ha una migliore sicurezza dei tipi (poichésync.Maputilizzaany).
Rarità: Non comune Difficoltà: Difficile


