Preparazione ai colloqui Go

Domande da Colloquio per Sviluppatori Backend Go

Una selezione di domande da colloquio su Go per sviluppatori backend, raggruppate per argomento ed estratte dallo stesso catalogo che alimenta la piattaforma di pratica EngineerSpeak.

Inizia un Colloquio AI per Backend GoNessuna carta di credito richiesta. 1 sessione gratuita disponibile.
Pratica per colloqui tecnici in ingleseUna modalità in cui chi non è madrelingua può esercitarsi a superare i colloqui tecnici.

Sistema di Tipi

1Spiega come funzionano i valori zero (zero value) di Go per i tipi integrati e di tipo riferimento, e perché sono importanti quando si dichiarano variabili senza un'inizializzazione esplicita.

In Go, una variabile dichiarata senza un inizializzatore esplicito viene automaticamente inizializzata al valore zero del suo tipo. I tipi numerici diventano 0, bool diventa false, string diventa "", e array o struct vengono azzerati elemento per elemento o campo per campo. I tipi simili a puntatori o riferimenti, come puntatori, slice, map, channel, funzioni e interfacce, hanno nil come valore zero. Questo è importante perché le variabili Go e i campi omessi di una struct iniziano in uno stato deterministico anziché contenere dati spazzatura, e molte API sono progettate in modo che il valore zero sia un'impostazione predefinita utile, sebbene alcuni valori nil richiedano comunque un'inizializzazione prima di eseguire determinate operazioni.

Prova a rispondere a questa domanda con un coach IA

2Come gestisce Go l'uguaglianza per le `struct` e cosa succede quando una `struct` contiene campi non comparabili?

I valori di tipo `struct` in Go possono essere confrontati tramite `==` e `!=` solo quando ogni campo della `struct` è comparabile. L'uguaglianza confronta i campi corrispondenti utilizzando la regola di uguaglianza specifica di ciascun campo. Se una `struct` contiene un campo non comparabile come una `slice`, una `map` o una funzione, il tipo `struct` non è comparabile e il confronto di due valori di quel tipo `struct` con `==` genera un errore a tempo di compilazione. Per tali `struct`, è necessario utilizzare una logica di confronto personalizzata o una funzione di supporto adeguata per l'uguaglianza profonda (deep-equality), specialmente nei test.

Prova a rispondere a questa domanda con un coach IA

3Spiega l'immutabilità delle stringhe in Go e la relazione tra string, []byte, bytes.Buffer e strings.Builder.

In Go, una `string` è una sequenza immutabile di byte, spesso testo UTF-8, sebbene non sia richiesto che sia UTF-8 valido. Non è possibile modificare una stringa sul posto; per alterarne il contenuto, in genere la si converte in `[]byte` per modifiche a livello di byte o in `[]rune` per modifiche a livello di code point, per poi convertirla nuovamente. Le normali conversioni tra `string` e `[]byte` copiano i dati e possono comportare allocazioni, perciò le conversioni ripetute o le concatenazioni continue all'interno di cicli possono risultare costose. `strings.Builder` è ottimizzato per costruire stringhe in modo efficiente, mentre `bytes.Buffer` è un buffer di byte mutabile utile per dati orientati ai byte e per l'I/O, ed è anche in grado di produrre una stringa.

Prova a rispondere a questa domanda con un coach IA

4Spiega come funziona nil in modo diverso per puntatori, slice, map, channel, funzioni e interfacce in Go.

In Go, nil è il valore zero per puntatori, slice, map, channel, funzioni e interfacce, ma le operazioni su questi valori nil differiscono in base al tipo. Un puntatore nil può essere confrontato con nil, ma la sua dereferenziazione causa un panic. Una slice nil ha lunghezza e capacità pari a 0 e supporta l'iterazione tramite range e l'aggiunta di elementi con append. Da una map nil è possibile leggere e iterare, ma l'assegnazione di valori causa un panic. Inviare o ricevere da un channel nil blocca l'esecuzione all'infinito, e la chiusura di un channel nil causa un panic. Chiamare una funzione nil causa un panic. Un'interfaccia è nil solo quando non ha né un tipo dinamico né un valore dinamico; un'interfaccia che contiene un valore nil tipizzato, come ad esempio un puntatore nil, non è a sua volta nil.

Prova a rispondere a questa domanda con un coach IA

5Cosa sono i tipi comparabili in Go e in che modo le regole di comparabilità influiscono sulle chiavi delle `map`, sull'uguaglianza e sui vincoli dei generics?

I tipi comparabili in Go sono tipi i cui valori possono essere confrontati tramite `==` e `!=`. I tipi base, i puntatori, i channel, le interfacce e le `struct`/gli array i cui campi o elementi sono a loro volta comparabili, sono tipi comparabili; i tipi `slice`, `map` e le funzioni non sono comparabili, tranne che rispetto a `nil`. Le chiavi delle `map` devono essere comparabili. L'uguaglianza segue le regole di confronto del tipo e il confronto tra interfacce dipende dai valori concreti dinamici; se un'interfaccia confrontata contiene un valore dinamico non comparabile, il confronto causa un panic. Nei generics, il vincolo predefinito `comparable` consente di confrontare i parametri di tipo tramite `==`/`!=` e di utilizzarli come chiavi per le `map`.

Prova a rispondere a questa domanda con un coach IA

6Come rappresenta Go i byte, le rune e il testo codificato in UTF-8, e perché len(s) può differire dal numero di caratteri visibili all'utente?

In Go, `byte` è un alias per `uint8` e rappresenta un singolo byte grezzo, mentre `rune` è un alias per `int32` e rappresenta un code point Unicode. Una `string` è una sequenza di byte di sola lettura, comunemente testo codificato in UTF-8, ma in grado di contenere byte arbitrari. `len(s)` restituisce il numero di byte, non di rune o di caratteri visibili all'utente. L'indicizzazione di una stringa restituisce un byte; iterare su una stringa con range decodifica l'UTF-8 e restituisce gli indici dei byte accompagnati dalle rune. `len(s)` può differire dal numero di caratteri visibili poiché l'UTF-8 può utilizzare più byte per ogni code point e perché un singolo carattere visibile all'utente può essere composto da più code point, come nel caso dei segni combinanti o delle sequenze di emoji.

Prova a rispondere a questa domanda con un coach IA

Strutture Dati

7Descrivi la differenza tra array e slice in Go, includendo il comportamento della lunghezza, della capacità e della memoria sottostante.

Un array in Go ha una lunghezza fissa che fa parte del suo tipo, come [3]int; memorizza i suoi elementi direttamente e l'assegnazione o il passaggio di un array copia l'intero valore dell'array. Una slice, come []int, è un piccolo descrittore di un array sottostante: concettualmente contiene un puntatore agli elementi, una lunghezza e una capacità. La lunghezza di una slice è il numero di elementi visibili; la sua capacità è quanti elementi possono essere utilizzati dall'inizio della slice prima di raggiungere la fine dell'array sottostante. Le slice sono flessibili: il reslicing modifica il descrittore e l'operazione di append può riutilizzare lo stesso array sottostante se la capacità lo consente, oppure allocarne uno nuovo in caso contrario.

Prova a rispondere a questa domanda con un coach IA

8Come si comporta il tipo `map` in Go per quanto riguarda i tipi di chiave, le chiavi mancanti, le `map` nulle (`nil`) e l'ordine di iterazione?

I tipi di chiave delle `map` in Go devono essere confrontabili; le `slice`, le `map` e le funzioni non possono essere usate direttamente come chiavi. La ricerca di una chiave non presente restituisce il valore zero del tipo di elemento, pertanto si utilizza l'idioma comma-ok (`v, ok := m[k]`) per distinguere l'assenza della chiave da un valore zero effettivamente presente. È possibile leggere e iterare su una `map` a `nil`, ma l'assegnazione causa un panic; deve essere inizializzata prima della scrittura. L'ordine di iterazione della `map` non è specificato e il codice non deve dipendere da esso.

Prova a rispondere a questa domanda con un coach IA

9Descrivi come il reslicing e l'assegnazione delle slice possano far sì che più slice condividano lo stesso array sottostante e quali bug ciò possa causare.

Il valore di una slice è un header che punta a un array sottostante. L'assegnazione di una slice o il suo passaggio a una funzione copia solo l'header, non gli elementi. Il reslicing crea un altro header che punta a un intervallo dello stesso array sottostante. Pertanto, più slice possono fare da alias alla stessa area di memoria: la modifica di un elemento tramite una slice può essere visibile attraverso un'altra e l'uso di append su una slice può sovrascrivere dati visibili a un'altra, se quest'ultima ha ancora capacità inutilizzata. I bug includono mutazioni impreviste, risultati corrotti, il mantenimento in memoria di grandi array sottostanti tramite piccole slice secondarie e data race quando gli alias vengono utilizzati in modo concorrente. Per evitare una condivisione involontaria, effettua una copia difensiva con copy o append([]T(nil), s...), oppure limita la capacità con un'espressione full-slice prima dell'uso di append.

Prova a rispondere a questa domanda con un coach IA

10Spiega la crescita delle `slice` durante l'uso di `append` a livello concettuale e le implicazioni sulle prestazioni delle riallocazioni ripetute.

Quando `append` aggiunge elementi a una `slice`, scrive nell'array di supporto esistente se la `slice` ha capacità sufficiente. Se la capacità è insufficiente, Go alloca un array di supporto più grande, copia gli elementi esistenti, scrive i nuovi elementi e restituisce l'intestazione della `slice` (slice header) che punta alla nuova area di memoria. L'esatta politica di crescita dipende dall'implementazione, ma concettualmente la capacità cresce in modo tale da rendere l'uso ripetuto di `append` ammortizzato ed efficiente. Le riallocazioni ripetute, tuttavia, consumano CPU per la copia, creano nuove allocazioni, aumentano la pressione sul GC (Garbage Collector) e possono interrompere la condivisione della memoria con vecchi alias della `slice`. Se si conosce la dimensione attesa, è opportuno pre-allocare con `make([]T, 0, n)` quando si costruisce tramite `append`, oppure con `make([]T, n)` quando si popola tramite indice, in modo da ridurre le riallocazioni.

Prova a rispondere a questa domanda con un coach IA

Semantica del Linguaggio

11Come funziona il blank identifier in Go per i valori non utilizzati, le importazioni e i controlli delle interfacce a tempo di compilazione?

Il blank identifier `_` è un segnaposto di sola scrittura. L'assegnazione a esso scarta il valore e non crea una variabile utilizzabile. Viene utilizzato per ignorare valori di ritorno o variabili di ciclo non necessari, per importare un pacchetto solo per i suoi effetti collaterali con `import _ "pkg"` e per effettuare controlli a tempo di compilazione sull'implementazione delle interfacce, come in `var _ io.Reader = (*MyReader)(nil)`. Un'importazione vuota (blank import) esegue comunque l'inizializzazione del pacchetto importato. Un'assegnazione per il controllo dell'interfaccia fallisce la compilazione se il method set del tipo concreto non soddisfa l'interfaccia.

Prova a rispondere a questa domanda con un coach IA

Pacchetti

12Come funziona l'ordine di inizializzazione dei pacchetti in Go, incluse le funzioni `init` e le dipendenze importate?

Go inizializza i pacchetti nell'ordine delle dipendenze. Le dipendenze importate di un pacchetto vengono inizializzate prima del pacchetto che le importa. All'interno di un pacchetto, le variabili a livello di pacchetto vengono inizializzate prima di qualsiasi funzione `init`, con l'inizializzazione delle variabili ordinata per dipendenza e ordine di dichiarazione, come definito dal linguaggio. Successivamente, le funzioni `init` del pacchetto vengono eseguite automaticamente; un pacchetto può avere più funzioni `init` e queste non possono essere chiamate direttamente. Ogni pacchetto viene inizializzato una sola volta. Per un eseguibile, viene inizializzato prima il grafo delle importazioni, poi viene inizializzato il pacchetto `main` e infine viene chiamata la funzione `main.main`.

Prova a rispondere a questa domanda con un coach IA

13Spiega le regole di visibilità dei pacchetti in Go, inclusi gli identificatori esportati e la convenzione della directory internal/.

In Go, la visibilità dei pacchetti è controllata dalla denominazione degli identificatori, non da parole chiave di accesso. Un identificatore il cui nome inizia con una lettera Unicode maiuscola è esportato e può essere referenziato da altri pacchetti; gli altri identificatori non sono esportati e sono utilizzabili solo all'interno dello stesso pacchetto. Questo si applica a funzioni, tipi, metodi, variabili, costanti e campi della struct. I pacchetti utilizzano gli identificatori esportati per definire la loro API pubblica e mantenere non esportati i dettagli di implementazione. Separatamente, un pacchetto situato sotto una directory `internal/` può essere importato solo da codice il cui percorso di importazione si trova all'interno dell'albero genitore di quella directory `internal`; ciò è imposto dalla toolchain di Go.

Prova a rispondere a questa domanda con un coach IA

Flusso di Controllo

14Come funziona defer in Go, considerando l'ordine di esecuzione, il momento della valutazione degli argomenti e l'interazione con i valori di ritorno?

`defer` programma l'esecuzione di una chiamata a funzione per il momento in cui la funzione circostante termina, indipendentemente dal fatto che termini per un normale ritorno o per lo svolgimento (unwinding) causato da un panic. Chiamate multiple differite (deferred) vengono eseguite in ordine last-in, first-out. Il valore della funzione differita e i suoi argomenti vengono valutati immediatamente quando viene eseguita l'istruzione `defer`, ma la chiamata stessa viene eseguita in seguito. Con valori di ritorno nominati (named return values), un'istruzione return assegna prima i valori di ritorno, poi vengono eseguite le funzioni differite; di conseguenza, una closure differita può osservare o modificare le variabili di risultato nominate prima che il chiamante le riceva. Questo rende `defer` utile per operazioni di pulizia come la chiusura di file, lo sblocco di mutex e il rilascio di risorse.

Prova a rispondere a questa domanda con un coach IA

Gestione degli Errori

15Spiega il modello di gestione degli errori in Go e i modi convenzionali in cui gli errori vengono creati, restituiti e controllati.

Go tratta gli errori come valori ordinari, non come eccezioni. L'interfaccia integrata `error` è soddisfatta da qualsiasi tipo che possieda un metodo `Error() string`. Per convenzione, le funzioni restituiscono un `error` come ultimo risultato, dove `nil` indica il successo e un errore diverso da `nil` indica che il chiamante deve gestire o propagare il fallimento. Gli errori semplici vengono comunemente creati con `errors.New`, gli errori formattati con `fmt.Errorf`, e i chiamanti solitamente li controllano usando `if err != nil { ... }`.

Prova a rispondere a questa domanda con un coach IA

16Come dovrebbe essere gestito il ripristino da panic (panic recovery) nei servizi backend in Go, incluso cosa succede quando una goroutine va in panic e quando un processo dovrebbe essere ripristinato rispetto ad andare in crash?

Un panic esegue l'unwinding della goroutine corrente, eseguendo le sue funzioni differite (deferred). `recover` funziona solo se chiamato da una funzione differita all'interno di quella stessa goroutine; una goroutine non può ripristinare il panic di un'altra goroutine. Se un panic non viene ripristinato, il processo va in crash. Nei servizi backend, il ripristino dovrebbe di solito essere posizionato in corrispondenza dei confini di isolamento, come i gestori delle richieste, i middleware RPC (Remote Procedure Call) o i punti di ingresso dei worker delle goroutine, in modo che una singola richiesta o un singolo job fallito non provochi il blocco dell'intero servizio. Tuttavia, se un panic potrebbe aver corrotto lo stato condiviso o reso inaffidabile l'integrità del processo, è più sicuro lasciare che il processo vada in crash e si riavvii piuttosto che ripristinarlo e continuare alla cieca.

Prova a rispondere a questa domanda con un coach IA

Concorrenza

17Cosa sono i canali nil in Go, e come possono compromettere accidentalmente il codice o disabilitare intenzionalmente i casi di una select?

Un canale nil è una variabile canale il cui valore è nil, spesso perché non è stata inizializzata con make o perché è stata esplicitamente impostata a nil. L'invio o la ricezione su un canale nil si blocca all'infinito. In una select, un case che coinvolge un canale nil non è mai pronto, quindi l'assegnazione di una variabile canale a nil può disabilitare intenzionalmente quel case. L'uso accidentale di un canale nil può bloccare (hang) le goroutine o impedire alla logica della select di gestire gli eventi attesi.

Prova a rispondere a questa domanda con un coach IA

18In che modo le operazioni atomiche in sync/atomic differiscono dalla sincronizzazione basata su mutex, e quando sono appropriate?

sync/atomic fornisce operazioni indivisibili su singole locazioni di memoria, come load, store, add, swap e compare-and-swap, con garanzie di sincronizzazione e di ordinamento della memoria (memory-ordering). Un mutex protegge una sezione critica, quindi può salvaguardare codice arbitrario e invarianti che coinvolgono letture, scritture o campi multipli. Le operazioni atomiche sono appropriate per stati semplici e indipendenti come contatori, flag, numeri di sequenza o strutture lock-free accuratamente progettate. È preferibile usare un mutex quando le operazioni sono composte, più valori devono rimanere coerenti o la versione atomica risulterebbe difficile da analizzare logicamente o da dimostrare corretta.

Prova a rispondere a questa domanda con un coach IA

19Come dovrebbero essere progettati la proprietà dei channel e il ciclo di vita delle goroutine per evitare leak di goroutine?

Progetta le goroutine con un proprietario esplicito, un segnale di spegnimento chiaro e un percorso di uscita garantito. Il lato produttore è generalmente responsabile della chiusura di un channel, specialmente se di output; i ricevitori non dovrebbero chiudere un channel mentre i mittenti potrebbero essere ancora attivi. Ogni operazione bloccante di invio, ricezione, ciclo, timer o chiamata esterna dovrebbe avere la garanzia di completamento o poter essere sbloccata in caso di annullamento, solitamente tramite context.Context o un done channel. Utilizza WaitGroup, errgroup o meccanismi di coordinazione simili in modo da attendere i worker e chiudere i channel solo dopo l'uscita dei mittenti.

Prova a rispondere a questa domanda con un coach IA

20Quali sono le cause comuni dei leak di goroutine nei servizi Go e come si rilevano e risolvono in produzione?

I leak di goroutine più comuni nei servizi Go derivano da goroutine bloccate per sempre sull'invio o sulla ricezione di un canale (channel), in attesa su altre operazioni bloccanti senza possibilità di cancellazione, I/O bloccato senza timeout (deadline), cicli in background o `ticker` che non si fermano mai, e goroutine legate all'ambito di una richiesta che sopravvivono alla richiesta stessa. In produzione, si cerca una crescita continua nel numero di goroutine e sintomi correlati, per poi ispezionare i dump o i profili `pprof` delle goroutine per capire dove siano rimaste bloccate. Risolvere il leak significa modificare il codice affinché tali goroutine possano terminare: aggiungere meccanismi di cancellazione e deadline, fermare i `ticker`, chiudere correttamente i canali, evitare goroutine distaccate per le richieste e limitare la concorrenza dove necessario.

Prova a rispondere a questa domanda con un coach IA