Domande di colloquio per sviluppatori Backend (Go): Guida completa

Milad Bonakdar
Autore
Padroneggia lo sviluppo backend in Go con domande essenziali per il colloquio che coprono concorrenza, interfacce, gestione degli errori e progettazione del sistema. Preparazione perfetta per i colloqui da sviluppatore Go.
Introduzione
Go (Golang) è diventato un linguaggio dominante per la creazione di sistemi backend scalabili, microservizi e applicazioni cloud-native. La sua semplicità, il solido modello di concorrenza e le prestazioni lo rendono una scelta ideale per i moderni team di ingegneria.
Questa guida copre le domande essenziali per i colloqui per sviluppatori Backend specializzati in Go. Esploreremo i concetti fondamentali del linguaggio, i modelli di concorrenza, la gestione degli errori e la progettazione del sistema per aiutarti a superare il tuo prossimo colloquio.
Concetti di base di Go
1. Cosa rende Go diverso da altri linguaggi come Java o Python?
Risposta: Go è stato progettato da Google per affrontare le sfide dello sviluppo di software su larga scala. Le differenze principali includono:
- Semplicità: Go ha un piccolo set di parole chiave e manca di funzionalità complesse come l'ereditarietà o l'overloading dei metodi, privilegiando la leggibilità.
- Concorrenza: Supporto di prima classe per la concorrenza tramite Goroutine e Canali, che semplifica la scrittura di programmi concorrenti scalabili.
- Compilazione: Go compila direttamente in codice macchina (binari collegati staticamente), offrendo velocità di avvio ed esecuzione elevate senza una macchina virtuale (JVM).
- Garbage Collection: Garbage collection efficiente ottimizzato per una bassa latenza.
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 è essenziale per la gestione di valori con ambito di richiesta, segnali di annullamento e scadenze tra i confini dell'API e le goroutine.
- Annullamento: Se un utente annulla una richiesta, il contesto viene annullato e tutte le goroutine generate che eseguono il lavoro per tale richiesta devono interrompersi per risparmiare risorse.
- Timeout:
context.WithTimeoutgarantisce che le query del database o le chiamate API esterne non si blocchino per sempre. - Valori: Può contenere dati specifici della richiesta come ID utente o token di autenticazione (usare con parsimonia).
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: Utilizzarli per ridurre la duplicazione del codice per algoritmi che si applicano a più tipi (come l'ordinamento, il filtraggio o strutture di dati come Set/Trees). Evitare l'uso eccessivo; se un'interfaccia è sufficiente, utilizzare quella.
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



