Preparazione per Go Middle

Domande da Colloquio per Sviluppatori Backend Go Middle

15 domande da colloquio selezionate su Go per sviluppatori backend Middle che devono spiegare compromessi pratici, concorrenza e comportamento dei servizi.

Inizia un Colloquio AI per Go MiddleNessuna 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 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

2Cosa 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

3Come 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

4In che modo gli alias di tipo differiscono dai tipi definiti e quando utilizzeresti ciascuno di essi?

Un tipo definito, come `type UserID int64`, crea un nuovo tipo distinto con `int64` come tipo sottostante. Non è assegnabile liberamente a `int64` senza una conversione e può avere i propri metodi. Un alias di tipo, come `type UserID = int64`, è semplicemente un altro nome per lo stesso tipo, quindi l'identità e l'assegnabilità del tipo sono preservate. Utilizza i tipi definiti per la modellazione del dominio, la type safety e i metodi; usa gli alias principalmente per il refactoring, le migrazioni o la compatibilità senza introdurre un nuovo tipo.

Prova a rispondere a questa domanda con un coach IA

5Come gestisce Go i valori speciali in virgola mobile e quali insidie legate all'uguaglianza sono rilevanti nei sistemi di backend?

I tipi `float32` e `float64` in Go utilizzano il comportamento in stile IEEE-754, inclusi valori speciali come infinito positivo/negativo e NaN (Not a Number). Per `float64`, le funzioni di utilità includono `math.Inf`, `math.IsInf`, `math.NaN` e `math.IsNaN`. Un valore NaN non è uguale a nulla, nemmeno a se stesso, quindi `x == x` è falso quando `x` è NaN. L'uguaglianza esatta sui numeri in virgola mobile calcolati è inoltre rischiosa perché l'arrotondamento e la precisione possono rendere diversi valori matematicamente uguali; è consigliabile utilizzare tolleranze appropriate al dominio o evitare i numeri in virgola mobile per valori aziendali o finanziari esatti come il denaro. I numeri in virgola mobile sono consentiti come chiavi nei tipi `map`, ma le chiavi NaN sono problematiche perché la ricerca in una mappa dipende dall'uguaglianza e NaN non risulta uguale a nessun valore, nemmeno a se stesso.

Prova a rispondere a questa domanda con un coach IA

6Descrivi l'incorporamento (embedding) delle `struct` in Go e come si comportano i campi e i metodi promossi.

L'incorporamento (embedding) di una `struct` in Go consiste nel dichiarare un campo tramite il suo tipo senza fornire un nome esplicito, ad esempio `type User struct { Person }`. Il valore incorporato è comunque un campo reale, accessibile come `u.Person`, ma i suoi campi e metodi esportati o accessibili possono essere promossi, in modo che i chiamanti possano scrivere selettori come `u.Name` o `u.Greet()` come scorciatoia rispetto al passaggio per il campo incorporato. L'incorporamento rappresenta una forma di composizione, non di ereditarietà classica: il tipo esterno non diventa automaticamente un sottotipo di quello incorporato. Se i selettori promossi entrano in conflitto, Go non tenta di indovinare; i nomi ambigui devono essere qualificati esplicitamente oppure non possono essere selezionati attraverso il valore esterno.

Prova a rispondere a questa domanda con un coach IA

Strutture Dati

7Descrivi 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

8Spiega 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

Pacchetti

9Quali sono i pattern di registrazione in fase di inizializzazione (init-time) in Go, e quali rischi derivano dagli effetti collaterali del blank import e dai registri globali?

Un pattern di registrazione in fase di inizializzazione si verifica quando un package registra un'implementazione all'interno di un registro condiviso da una funzione `init`. Un blank import, come `_ "example.com/driver"`, viene spesso utilizzato per importare un package solo per i suoi effetti collaterali, causando l'esecuzione della sua funzione `init` anche se non viene referenziato alcun nome esportato. Ciò è comune per punti di estensione come driver, codec, plugin, metriche o serializzatori. I rischi includono dipendenze nascoste ed effetti collaterali all'avvio, stato mutabile globale, registrazione duplicata o sensibile all'ordine, maggiore difficoltà nell'isolamento dei test e un cablaggio delle dipendenze meno esplicito. Questo pattern dovrebbe essere utilizzato in modo deliberato, ben documentato e spesso mitigato con registrazioni esplicite, registri idempotenti e sicuri in concorrenza (concurrency-safe), o registri iniettabili/resettabili per i test.

Prova a rispondere a questa domanda con un coach IA

Gestione degli Errori

10In che modo defer, panic e i valori di ritorno nominati interagiscono durante l'implementazione di logiche di pulizia che potrebbero modificare gli errori restituiti?

Le funzioni deferite (deferred functions) vengono eseguite dopo che i valori di ritorno sono stati assegnati, ma prima che la funzione ritorni al suo chiamante. Per questo motivo, una chiusura (closure) deferita può leggere o modificare valori di ritorno nominati, come ad esempio un `err` nominato. Questo comportamento è comunemente usato per aggiungere errori generati da procedure di pulizia come `Close`, `Commit` o operazioni simili all'errore che sta per essere restituito, idealmente preservando l'errore principale invece di sovrascriverlo. Durante l'unwinding dello stack causato da un panic, le funzioni deferite vengono comunque eseguite; una funzione deferita può eseguire un `recover` e impostare un valore di ritorno nominato, ma questa pratica dovrebbe essere limitata a limiti intenzionali per la gestione del panic. Bisogna fare attenzione a non oscurare (shadowing) una variabile di ritorno nominata come `err`, altrimenti il `defer` potrebbe osservare o modificare una variabile diversa da quella intesa.

Prova a rispondere a questa domanda con un coach IA

11Come funzionano errors.Is, errors.As e %w nelle catene di wrapping degli errori in Go?

`fmt.Errorf` con `%w` crea un nuovo errore che avvolge un errore sottostante aggiungendo del contesto. I wrapper espongono gli errori sottostanti tramite `Unwrap`, formando una catena o un albero che la libreria standard può ispezionare. `errors.Is(err, target)` verifica se `err` o qualsiasi errore da esso avvolto corrisponda a un errore target. `errors.As(err, &target)` verifica se `err` o qualsiasi errore da esso avvolto sia assegnabile al tipo target e memorizza il valore corrispondente nel puntatore fornito.

Prova a rispondere a questa domanda con un coach IA

12Cosa sono gli errori sentinella (sentinel errors) e quali sono i compromessi rispetto agli errori tipizzati personalizzati o a modelli di errore di dominio più ricchi?

Un errore sentinella (sentinel error) è un valore di errore con un nome, spesso una variabile a livello di pacchetto come `var ErrNotFound = errors.New("not found")`, utilizzato per rappresentare una condizione specifica che i chiamanti possono testare, tipicamente con `errors.Is` quando è possibile l'incapsulamento dell'errore (wrapping). Le sentinelle sono semplici e utili per ampie categorie stabili, ma le sentinelle esportate diventano parte dell'API e possono accoppiare fortemente i chiamanti a valori particolari. Gli errori tipizzati personalizzati possono trasportare campi strutturati ed essere individuati con `errors.As`. I modelli di errore di dominio più ricchi classificano i fallimenti per tipologia o codice e possono includere messaggi sicuri o metadati, il che risulta utile quando i chiamanti necessitano di un comportamento stabile che vada oltre un singolo valore di errore fisso.

Prova a rispondere a questa domanda con un coach IA

13Come dovrebbe un backend Go mappare gli errori interni in risposte utili per il client, fornendo al contempo agli operatori segnali diagnosticabili?

Un backend Go dovrebbe tradurre gli errori interni in corrispondenza del confine dell'applicazione o del trasporto in categorie e risposte stabili e sicure per il client. Queste categorie dovrebbero mappare a codici di stato HTTP (Hypertext Transfer Protocol) appropriati o a stati di trasporto equivalenti, fornendo messaggi sicuri e codici leggibili dalle macchine anziché restituire gli errori interni non elaborati. Gli operatori dovrebbero comunque poter accedere alle informazioni diagnostiche attraverso log strutturati, trace, metriche, ID di correlazione o di richiesta e la conservazione delle cause sottostanti l'errore. Il logging viene solitamente eseguito preferibilmente una sola volta a un confine che disponga del contesto della richiesta, in modo da evitare sia di omettere dei fallimenti sia di generare log duplicati rumorosi.

Prova a rispondere a questa domanda con un coach IA

14Come si implementano correttamente i tipi di errore personalizzati in Go e in che modo `errors.Join` influisce sull'ispezione e sulla gestione degli errori nelle fasi di pulizia (cleanup path)?

Un tipo di errore personalizzato in Go soddisfa l'interfaccia `error` implementando `Error() string`. Può anche contenere campi strutturati e implementare `Unwrap() error` per esporre la causa originaria. La scelta tra ricevitori a puntatore o a valore è rilevante: un ricevitore a puntatore significa che solo `*T` soddisfa l'interfaccia `error`, mentre un ricevitore a valore in genere significa che sia `T` sia `*T` la soddisfano, il che influisce sulla copia e sul tipo che le funzioni chiamanti dovrebbero utilizzare con `errors.As`. `errors.Join` combina più errori in un unico errore; `errors.Is` e `errors.As` consentono di ispezionare tutti gli errori uniti. Questo risulta utile quando è necessario restituire sia l'errore di un'operazione principale sia un errore in fase di pulizia (spesso generato tramite `defer`) senza perdere traccia di nessuno dei due fallimenti.

Prova a rispondere a questa domanda con un coach IA

Concorrenza

15Come funziona `select` con i channel, includendo la scelta dei casi pronti, i casi di default e le operazioni annullabili?

L'istruzione `select` attende su più operazioni su channel ed esegue un caso (case) in cui l'invio o la ricezione possono procedere. Se nessun caso è pronto, si blocca a meno che non sia presente un caso `default`; il `default` viene eseguito immediatamente solo quando nessuna operazione sul channel può procedere, il che è utile per tentativi di invio o ricezione non bloccanti. Se più casi sono pronti, Go ne sceglie uno in modo pseudo-casuale piuttosto che in base all'ordine nel codice sorgente. Le operazioni sui channel annullabili aggiungono solitamente un caso che riceve da `ctx.Done()`, in modo che la goroutine possa smettere di attendere quando il contesto viene annullato o va in timeout.

Prova a rispondere a questa domanda con un coach IA